updateweb

This commit is contained in:
Local Server
2026-01-01 22:24:30 -06:00
parent 017c6376fc
commit 1919f6f8bb
185 changed files with 19860 additions and 17603 deletions

View File

@@ -24,46 +24,46 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html" class="active"
<a href="/admin/blog" class="active"
><i class="bi bi-newspaper"></i> Blog</a
>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -1,5 +1,5 @@
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--primary-gradient: #202023;
--sidebar-width: 250px;
--transition-speed: 0.3s;
--primary-color: #667eea;
@@ -43,6 +43,8 @@ body {
margin-bottom: 30px;
text-align: center;
padding: 10px;
color: white;
letter-spacing: 1px;
}
.sidebar-menu {
@@ -56,26 +58,28 @@ body {
}
.sidebar-menu a {
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
display: flex;
align-items: center;
padding: 12px 15px;
border-radius: 8px;
transition: all 0.25s ease;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.sidebar-menu a:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.sidebar-menu a.active {
background: rgba(255, 255, 255, 0.2);
color: white;
background: #667eea;
color: #ffffff;
font-weight: 600;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.sidebar-menu i {

View File

@@ -15,7 +15,7 @@
/>
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--primary-gradient: #202023;
--sidebar-width: 250px;
--transition-speed: 0.3s;
}
@@ -54,6 +54,8 @@
margin-bottom: 30px;
text-align: center;
padding: 10px;
color: white;
letter-spacing: 1px;
}
.sidebar-menu {
@@ -67,26 +69,28 @@
}
.sidebar-menu a {
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
display: flex;
align-items: center;
padding: 12px 15px;
border-radius: 8px;
transition: all 0.25s ease;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.sidebar-menu a:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.sidebar-menu a.active {
background: rgba(255, 255, 255, 0.2);
color: white;
background: #667eea;
color: #ffffff;
font-weight: 600;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.sidebar-menu i {
@@ -380,45 +384,43 @@
<body>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html" class="active"
<a href="/admin/dashboard" class="active"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
><i class="bi bi-easel"></i> Portfolio</a
>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"><i class="bi bi-file-text"></i> Pages</a>
<a href="/admin/pages"><i class="bi bi-file-text"></i> Pages</a>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>
@@ -432,7 +434,7 @@
<p class="mb-0 text-muted">Manage your online shop</p>
</div>
<div>
<a href="/index.html" target="_blank" class="btn-view-site me-2"
<a href="/" target="_blank" class="btn-view-site me-2"
><i class="bi bi-eye"></i> View Site</a
>
<button class="btn-logout" id="logoutBtn">

View File

@@ -148,44 +148,44 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html" class="active"
<a href="/admin/homepage" class="active"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -525,11 +525,15 @@ function renderImageVariants() {
// Edit product
async function editProduct(id) {
try {
console.log("Loading product for edit, ID:", id);
const response = await fetch(`/api/admin/products/${id}`, {
credentials: "include",
});
const data = await response.json();
console.log("Product data loaded:", data);
if (data.success) {
const product = data.product;
@@ -561,6 +565,7 @@ async function editProduct(id) {
// Load image variants and extract unique product images
imageVariants = product.images || [];
console.log("Loaded image variants:", imageVariants);
// Build productImages array from unique image URLs in variants
const uniqueImages = {};
@@ -579,33 +584,77 @@ async function editProduct(id) {
renderImageVariants();
productModal.show();
} else {
showError(data.message || "Failed to load product");
}
} catch (error) {
console.error("Failed to load product:", error);
showError("Failed to load product details");
showError("Failed to load product details: " + error.message);
}
}
// Save product
async function saveProduct() {
const id = document.getElementById("productId").value;
const saveButton = document.getElementById("btnSaveProduct");
// Disable button and show loading state
saveButton.disabled = true;
const originalButtonHTML = saveButton.innerHTML;
saveButton.innerHTML = '<i class="bi bi-hourglass-split"></i> Saving...';
saveButton.style.opacity = "0.7";
// Get description from Quill editor
const description = quillEditor.root.innerHTML;
// Prepare images array for backend with all new fields
const images = imageVariants.map((variant, index) => ({
image_url: variant.image_url,
color_variant: variant.color_variant || null,
color_code: variant.color_code || null,
alt_text: variant.alt_text || document.getElementById("productName").value,
display_order: index,
is_primary: variant.is_primary || false,
variant_price: variant.variant_price
? parseFloat(variant.variant_price)
: null,
variant_stock: parseInt(variant.variant_stock) || 0,
}));
console.log("=== SAVE PRODUCT DEBUG ===");
console.log("Product ID:", id);
console.log("Image Variants:", imageVariants);
console.log("Product Images:", productImages);
// Prepare images array - include BOTH product images AND color variants
const images = [];
// First, add all color variants (they have color_variant/color_code)
imageVariants.forEach((variant, index) => {
images.push({
image_url: variant.image_url,
color_variant: variant.color_variant || null,
color_code: variant.color_code || null,
alt_text:
variant.alt_text || document.getElementById("productName").value,
display_order: index,
is_primary:
variant.is_primary || (index === 0 && imageVariants.length > 0),
variant_price: variant.variant_price
? parseFloat(variant.variant_price)
: null,
variant_stock: parseInt(variant.variant_stock) || 0,
});
});
// Then add regular product images (without color variants)
// Skip images that are already in imageVariants
const variantUrls = imageVariants.map((v) => v.image_url);
productImages.forEach((img, index) => {
if (!variantUrls.includes(img.url)) {
images.push({
image_url: img.url,
color_variant: null,
color_code: null,
alt_text:
img.alt_text ||
img.filename ||
document.getElementById("productName").value,
display_order: imageVariants.length + index,
is_primary: images.length === 0, // First image overall is primary
variant_price: null,
variant_stock: 0,
});
}
});
console.log("Prepared images array (variants + regular):", images);
const formData = {
name: document.getElementById("productName").value,
@@ -624,17 +673,41 @@ async function saveProduct() {
images: images,
};
console.log("Form Data:", formData);
// Validation
if (!formData.name || !formData.price) {
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError("Please fill in all required fields (Name and Price)");
return;
}
if (
imageVariants.length > 0 &&
imageVariants.some((v) => !v.image_url || !v.color_variant)
) {
showError("All color variants must have an image and color name selected");
// Validate that we have at least one image (either variant or regular)
if (images.length === 0) {
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError("Please add at least one product image");
return;
}
// Only validate image URLs exist
if (images.some((img) => !img.image_url)) {
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError("All images must have a valid image URL");
return;
}
// If a color_variant name is provided, require color_code
if (images.some((img) => img.color_variant && !img.color_code)) {
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError("All color variants must have both a color name and color code");
return;
}
@@ -642,6 +715,8 @@ async function saveProduct() {
const url = id ? `/api/admin/products/${id}` : "/api/admin/products";
const method = id ? "PUT" : "POST";
console.log("Saving product:", method, url, formData);
const response = await fetch(url, {
method: method,
headers: {
@@ -651,21 +726,58 @@ async function saveProduct() {
body: JSON.stringify(formData),
});
console.log("Response status:", response.status);
if (!response.ok) {
const errorText = await response.text();
console.error("Response error:", errorText);
throw new Error(`Server error: ${response.status} - ${errorText}`);
}
const data = await response.json();
console.log("Response data:", data);
if (data.success) {
// Change button to success state briefly
saveButton.innerHTML = '<i class="bi bi-check-circle-fill"></i> Saved!';
saveButton.style.background = "#10b981";
// Show success toast
showSuccess(
id
? "Product updated successfully! 🎉"
: "Product created successfully! 🎉"
? "Product Updated Successfully! Changes are now live on your website."
: "Product Created Successfully! Now visible on your shop page."
);
productModal.hide();
loadProducts();
// Wait a moment then close modal
setTimeout(async () => {
productModal.hide();
// Reload products to show updated data
await loadProducts();
// Clear form for next use
imageVariants = [];
productImages = [];
// Reset button
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
saveButton.style.background = "";
}, 1000);
} else {
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError(data.message || "Failed to save product");
}
} catch (error) {
console.error("Failed to save product:", error);
showError("Failed to save product");
saveButton.disabled = false;
saveButton.innerHTML = originalButtonHTML;
saveButton.style.opacity = "1";
showError(`Failed to save product: ${error.message}`);
}
}
@@ -841,14 +953,18 @@ function formatDate(dateString) {
}
function showSuccess(message) {
console.log("✅ SUCCESS:", message);
showToast(message, "success");
}
function showError(message) {
console.error("❌ ERROR:", message);
showToast(message, "error");
}
function showToast(message, type = "info") {
console.log("📢 Showing toast:", type, message);
// Create toast container if it doesn't exist
let container = document.getElementById("toastContainer");
if (!container) {
@@ -856,6 +972,7 @@ function showToast(message, type = "info") {
container.id = "toastContainer";
container.className = "toast-container";
document.body.appendChild(container);
console.log("Created toast container");
}
// Create toast element
@@ -883,6 +1000,7 @@ function showToast(message, type = "info") {
`;
container.appendChild(toast);
console.log("Toast appended to container");
// Auto remove after 4 seconds
setTimeout(() => {

View File

@@ -84,19 +84,19 @@ function renderUsers(users) {
${u.isactive ? "Active" : "Disabled"}</span></td>
<td>${formatDate(u.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editUser(${
<button class="btn btn-sm btn-info" onclick="editUser('${escapeHtml(
u.id
})" title="Edit User">
)}')" title="Edit User">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-warning" onclick="showChangePassword(${
<button class="btn btn-sm btn-warning" onclick="showChangePassword('${escapeHtml(
u.id
}, '${escapeHtml(u.name)}')" title="Change Password">
)}', '${escapeHtml(u.name)}')" title="Change Password">
<i class="bi bi-key"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser(${
<button class="btn btn-sm btn-danger" onclick="deleteUser('${escapeHtml(
u.id
}, '${escapeHtml(u.name)}')" title="Delete User">
)}', '${escapeHtml(u.name)}')" title="Delete User">
<i class="bi bi-trash"></i>
</button>
</td>
@@ -195,6 +195,8 @@ async function saveUser() {
return;
}
showLoading(id ? "Updating user..." : "Creating user...");
try {
const url = id ? `/api/admin/users/${id}` : "/api/admin/users";
const method = id ? "PUT" : "POST";
@@ -206,6 +208,8 @@ async function saveUser() {
});
const data = await response.json();
hideLoading();
if (data.success) {
showSuccess(
id ? "User updated successfully" : "User created successfully"
@@ -217,6 +221,7 @@ async function saveUser() {
}
} catch (error) {
console.error("Failed to save user:", error);
hideLoading();
showError("Failed to save user");
}
}
@@ -249,6 +254,8 @@ async function changePassword() {
return;
}
showLoading("Changing password...");
try {
const response = await fetch(`/api/admin/users/${id}/password`, {
method: "PUT",
@@ -258,6 +265,8 @@ async function changePassword() {
});
const data = await response.json();
hideLoading();
if (data.success) {
showSuccess("Password changed successfully");
passwordModal.hide();
@@ -266,6 +275,7 @@ async function changePassword() {
}
} catch (error) {
console.error("Failed to change password:", error);
hideLoading();
showError("Failed to change password");
}
}
@@ -278,12 +288,16 @@ async function deleteUser(id, name) {
)
return;
showLoading("Deleting user...");
try {
const response = await fetch(`/api/admin/users/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
hideLoading();
if (data.success) {
showSuccess("User deleted successfully");
loadUsers();
@@ -292,6 +306,7 @@ async function deleteUser(id, name) {
}
} catch (error) {
console.error("Failed to delete user:", error);
hideLoading();
showError("Failed to delete user");
}
}
@@ -332,9 +347,76 @@ function formatDate(dateString) {
});
}
// Custom notification system
function showNotification(type, title, message) {
const container = document.getElementById("notificationContainer");
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
const icon =
type === "success"
? '<i class="bi bi-check-circle-fill"></i>'
: '<i class="bi bi-exclamation-circle-fill"></i>';
notification.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<div class="notification-title">${title}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="closeNotification(this)">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.classList.add("removing");
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}
}, 3000);
}
function closeNotification(button) {
const notification = button.closest(".notification");
notification.classList.add("removing");
setTimeout(() => {
notification.remove();
}, 300);
}
function showSuccess(message) {
alert(message);
showNotification("success", "Success!", message);
}
function showError(message) {
alert("Error: " + message);
showNotification("error", "Error", message);
}
// Loading overlay functions
function showLoading(message = "Saving...") {
const overlay = document.createElement("div");
overlay.className = "loading-overlay";
overlay.id = "loadingOverlay";
overlay.innerHTML = `
<div class="loading-spinner-container">
<div class="loading-spinner-icon"></div>
<div class="loading-spinner-text">${message}</div>
</div>
`;
document.body.appendChild(overlay);
}
function hideLoading() {
const overlay = document.getElementById("loadingOverlay");
if (overlay) {
overlay.remove();
}
}

View File

@@ -5,121 +5,261 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Login - Sky Art Shop</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css"
/>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.login-container {
display: flex;
height: 100vh;
}
.logo-section {
flex: 1;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
.login-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 400px;
width: 100%;
}
.login-header {
.logo-content {
text-align: center;
margin-bottom: 30px;
max-width: 1100px;
}
.login-header h1 {
color: #2c3e50;
font-size: 28px;
.logo-image {
width: 100%;
max-width: 1000px;
height: auto;
margin-bottom: 20px;
}
.form-section {
flex: 1;
background-color: #ffd0d0;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
position: relative;
}
.login-box {
background: white;
border: 2px solid #333;
padding: 60px 50px;
width: 100%;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.login-title {
font-size: 2.5rem;
font-weight: bold;
text-align: center;
margin-bottom: 10px;
color: #333;
}
.login-header p {
color: #7f8c8d;
margin: 0;
.login-subtitle {
font-size: 1.1rem;
color: #666;
text-align: center;
margin-bottom: 40px;
}
.form-label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 1rem;
}
.form-control {
border-radius: 8px;
padding: 12px;
border: 2px solid #e0e0e0;
border: 2px solid #ddd;
padding: 12px 15px;
font-size: 1rem;
border-radius: 4px;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
border-color: #333;
box-shadow: none;
}
.btn-group-custom {
display: flex;
gap: 15px;
margin-top: 30px;
margin-bottom: 30px;
}
.btn-custom {
flex: 1;
padding: 12px 20px;
font-size: 1rem;
font-weight: 600;
border: 2px solid #333;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.btn-login {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
padding: 12px;
color: white;
font-weight: 600;
width: 100%;
transition: transform 0.2s;
background-color: white;
color: #333;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
background-color: #333;
color: white;
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
.btn-reset {
background-color: white;
color: #333;
}
.btn-reset:hover {
background-color: #333;
color: white;
}
.back-link {
text-align: center;
margin-top: 20px;
}
.back-link a {
color: #ff6b6b;
text-decoration: none;
font-size: 1.1rem;
font-weight: 500;
}
.back-link a:hover {
color: #ff5252;
text-decoration: underline;
}
.alert {
border-radius: 8px;
margin-bottom: 25px;
border-radius: 4px;
padding: 12px 15px;
display: none;
}
.alert.show {
display: block;
}
@media (max-width: 992px) {
.login-container {
flex-direction: column;
}
.logo-section,
.form-section {
flex: none;
height: 50vh;
}
.color-code {
font-size: 18px;
}
.login-box {
padding: 40px 30px;
}
}
@media (max-width: 576px) {
.login-box {
padding: 30px 20px;
}
.login-title {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="login-header">
<h1>🛍️ Sky Art Shop</h1>
<p>Admin Panel Login</p>
<div class="login-container">
<!-- Left Section - Logo -->
<div class="logo-section">
<div class="logo-content">
<img
src="/uploads/cat-logo-template-page-20251224-194356-0000-1766724728795-173839741.png"
alt="Sky Art Shop Logo"
class="logo-image"
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
/>
<div style="display: none">
<svg
width="400"
height="300"
viewBox="0 0 400 300"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Cat Silhouette -->
<path
d="M120 80 Q100 60 90 80 L80 100 Q70 120 80 140 L100 160 Q110 180 140 180 L160 185 Q180 190 200 180 L220 160 Q230 140 220 120 L210 100 Q200 80 180 80 Z"
fill="#000"
/>
<circle cx="110" cy="100" r="8" fill="#000" />
<circle cx="170" cy="100" r="8" fill="#000" />
<!-- Text -->
<text
x="50"
y="240"
font-family="'Brush Script MT', cursive"
font-size="48"
fill="#000"
>
Sky Art Shop
</text>
</svg>
</div>
</div>
</div>
<div class="alert alert-danger" role="alert" id="errorAlert"></div>
<!-- Right Section - Form -->
<div class="form-section">
<div class="login-box">
<h1 class="login-title">Sky Art Shop</h1>
<p class="login-subtitle">Admin Panel Login</p>
<form id="loginForm">
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input
type="email"
class="form-control"
id="email"
name="email"
required
placeholder="admin@example.com"
autocomplete="username"
/>
</div>
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
id="password"
name="password"
required
placeholder="Enter your password"
autocomplete="current-password"
/>
</div>
<button type="submit" class="btn btn-login" id="loginBtn">Sign In</button>
</form>
<div class="alert alert-danger" role="alert" id="errorAlert"></div>
<div class="text-center mt-4">
<a
href="/home.html"
class="text-decoration-none"
style="color: #667eea"
>← Back to Website</a
>
<form id="loginForm">
<div class="mb-3">
<label for="email" class="form-label">Username:</label>
<input
type="email"
class="form-control"
id="email"
name="email"
required
placeholder="Enter your email"
autocomplete="username"
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input
type="password"
class="form-control"
id="password"
name="password"
required
placeholder="Enter your password"
autocomplete="current-password"
/>
</div>
<div class="btn-group-custom">
<button type="submit" class="btn-custom btn-login" id="loginBtn">
Login
</button>
<button type="reset" class="btn-custom btn-reset">Reset</button>
</div>
</form>
<div class="back-link">
<a href="/">Back to Website</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
document
.getElementById("loginForm")
@@ -133,8 +273,8 @@
// Disable button during login
loginBtn.disabled = true;
loginBtn.textContent = "Signing in...";
errorAlert.style.display = "none";
loginBtn.textContent = "Logging in...";
errorAlert.classList.remove("show");
try {
const response = await fetch("/api/admin/login", {
@@ -150,20 +290,23 @@
if (response.ok && data.success) {
// Login successful - redirect to dashboard
window.location.href = "/admin/dashboard.html";
window.location.href = "/admin/dashboard";
} else {
// Show error
errorAlert.textContent = data.message || "Invalid credentials";
errorAlert.style.display = "block";
errorAlert.innerHTML =
'<i class="bi bi-exclamation-triangle"></i> ' +
(data.message || "Invalid credentials");
errorAlert.classList.add("show");
loginBtn.disabled = false;
loginBtn.textContent = "Sign In";
loginBtn.textContent = "Login";
}
} catch (error) {
console.error("Login error:", error);
errorAlert.textContent = "Login failed. Please try again.";
errorAlert.style.display = "block";
errorAlert.innerHTML =
'<i class="bi bi-exclamation-triangle"></i> Login failed. Please try again.';
errorAlert.classList.add("show");
loginBtn.disabled = false;
loginBtn.textContent = "Sign In";
loginBtn.textContent = "Login";
}
});
</script>

View File

@@ -475,44 +475,44 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html" class="active"
<a href="/admin/media-library" class="active"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>
@@ -790,7 +790,7 @@
);
allFolders = [];
if (data.message && data.message.includes("Authentication")) {
window.location.href = "/admin/login.html";
window.location.href = "/admin/login";
}
}
} catch (error) {
@@ -833,7 +833,7 @@
console.error("Failed to load files:", data.message || data.error);
allFiles = [];
if (data.message && data.message.includes("Authentication")) {
window.location.href = "/admin/login.html";
window.location.href = "/admin/login";
}
}
} catch (error) {

View File

@@ -49,46 +49,46 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html" class="active"
<a href="/admin/menu" class="active"
><i class="bi bi-list"></i> Menu</a
>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -233,44 +233,44 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html" class="active"
<a href="/admin/pages" class="active"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -24,44 +24,44 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html" class="active"
<a href="/admin/portfolio" class="active"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -25,46 +25,46 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html" class="active"
<a href="/admin/products" class="active"
><i class="bi bi-box"></i> Products</a
>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -124,46 +124,46 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html" class="active"
<a href="/admin/settings" class="active"
><i class="bi bi-gear"></i> Settings</a
>
</li>
<li>
<a href="/admin/users.html"><i class="bi bi-people"></i> Users</a>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>

View File

@@ -115,43 +115,43 @@
</button>
</div>
<nav class="sidebar-nav">
<a href="/admin/dashboard.html" class="nav-item">
<a href="/admin/dashboard" class="nav-item">
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
</a>
<a href="/admin/products.html" class="nav-item">
<a href="/admin/products" class="nav-item">
<i class="bi bi-box-seam"></i>
<span>Products</span>
</a>
<a href="/admin/portfolio.html" class="nav-item">
<a href="/admin/portfolio" class="nav-item">
<i class="bi bi-images"></i>
<span>Portfolio</span>
</a>
<a href="/admin/blog.html" class="nav-item">
<a href="/admin/blog" class="nav-item">
<i class="bi bi-file-text"></i>
<span>Blog</span>
</a>
<a href="/admin/pages.html" class="nav-item">
<a href="/admin/pages" class="nav-item">
<i class="bi bi-file-earmark"></i>
<span>Pages</span>
</a>
<a href="/admin/team-members.html" class="nav-item active">
<a href="/admin/team-members" class="nav-item active">
<i class="bi bi-people"></i>
<span>Team Members</span>
</a>
<a href="/admin/media-library.html" class="nav-item">
<a href="/admin/media-library" class="nav-item">
<i class="bi bi-image"></i>
<span>Media Library</span>
</a>
<a href="/admin/menu.html" class="nav-item">
<a href="/admin/menu" class="nav-item">
<i class="bi bi-list"></i>
<span>Menu</span>
</a>
<a href="/admin/users.html" class="nav-item">
<a href="/admin/users" class="nav-item">
<i class="bi bi-person"></i>
<span>Users</span>
</a>
<a href="/admin/settings.html" class="nav-item">
<a href="/admin/settings" class="nav-item">
<i class="bi bi-gear"></i>
<span>Settings</span>
</a>

View File

@@ -0,0 +1,465 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notification System Demo</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.demo-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
button {
padding: 15px 25px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-error {
background: #ef4444;
color: white;
}
.btn-error:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-loading {
background: #667eea;
color: white;
}
.btn-loading:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.info-box {
background: #eff6ff;
border-left: 4px solid #3b82f6;
padding: 20px;
border-radius: 8px;
margin-top: 30px;
}
.info-box h3 {
margin-top: 0;
color: #1e40af;
}
.info-box ul {
margin-bottom: 0;
}
.info-box li {
margin-bottom: 8px;
color: #334155;
}
/* Notification Styles (copied from users.html) */
#notificationContainer {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
}
.notification {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
margin-bottom: 12px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: white;
border-left: 4px solid;
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s;
opacity: 1;
transform: translateX(0);
min-width: 320px;
}
.notification.removing {
animation: slideOut 0.3s ease-in;
}
.notification-success {
border-left-color: #10b981;
background: linear-gradient(to right, #f0fdf4 0%, #ffffff 100%);
}
.notification-error {
border-left-color: #ef4444;
background: linear-gradient(to right, #fef2f2 0%, #ffffff 100%);
}
.notification-icon {
font-size: 24px;
flex-shrink: 0;
}
.notification-success .notification-icon {
color: #10b981;
}
.notification-error .notification-icon {
color: #ef4444;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
color: #111;
}
.notification-message {
font-size: 13px;
color: #666;
line-height: 1.4;
}
.notification-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
flex-shrink: 0;
}
.notification-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #333;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0.7;
}
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
backdrop-filter: blur(2px);
}
.loading-spinner-container {
background: white;
padding: 30px 40px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
text-align: center;
}
.loading-spinner-icon {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
.loading-spinner-text {
color: #333;
font-size: 16px;
font-weight: 500;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="demo-container">
<h1>🎨 Custom Notification System</h1>
<p class="subtitle">
Test the new notification system for user management
</p>
<div class="button-grid">
<button class="btn-success" onclick="testSuccess()">
<i class="bi bi-check-circle"></i>
Show Success
</button>
<button class="btn-error" onclick="testError()">
<i class="bi bi-x-circle"></i>
Show Error
</button>
<button class="btn-loading" onclick="testLoading()">
<i class="bi bi-hourglass-split"></i>
Show Loading
</button>
<button class="btn-success" onclick="testMultiple()">
<i class="bi bi-stack"></i>
Multiple Notifications
</button>
</div>
<div class="info-box">
<h3>✨ Features</h3>
<ul>
<li>
<strong>Custom Design:</strong> Beautiful, non-intrusive
notifications
</li>
<li>
<strong>Auto-dismiss:</strong> Notifications automatically fade
after 3 seconds
</li>
<li>
<strong>Manual Close:</strong> Click the X button to close
immediately
</li>
<li>
<strong>Loading Overlay:</strong> Full-screen loading indicator with
spinner
</li>
<li>
<strong>Smooth Animations:</strong> Slide-in and slide-out
transitions
</li>
<li>
<strong>Stack Support:</strong> Multiple notifications stack nicely
</li>
<li>
<strong>No Browser Alerts:</strong> Replaced with styled, modern
notifications
</li>
</ul>
</div>
<div
class="info-box"
style="
background: #fef9c3;
border-left-color: #eab308;
margin-top: 15px;
"
>
<h3 style="color: #854d0e">💡 Usage in User Management</h3>
<ul>
<li>✅ Creating user → Shows loading, then success notification</li>
<li>✅ Updating user → Shows loading, then success notification</li>
<li>
✅ Changing password → Shows loading, then success notification
</li>
<li>✅ Deleting user → Shows loading, then success notification</li>
<li>❌ Any error → Shows error notification with details</li>
</ul>
</div>
</div>
<!-- Notification Container -->
<div id="notificationContainer"></div>
<script>
// Custom notification system
function showNotification(type, title, message) {
const container = document.getElementById("notificationContainer");
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
const icon =
type === "success"
? '<i class="bi bi-check-circle-fill"></i>'
: '<i class="bi bi-exclamation-circle-fill"></i>';
notification.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<div class="notification-title">${title}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="closeNotification(this)">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.classList.add("removing");
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}
}, 3000);
}
function closeNotification(button) {
const notification = button.closest(".notification");
notification.classList.add("removing");
setTimeout(() => {
notification.remove();
}, 300);
}
function showLoading(message = "Loading...") {
const overlay = document.createElement("div");
overlay.className = "loading-overlay";
overlay.id = "loadingOverlay";
overlay.innerHTML = `
<div class="loading-spinner-container">
<div class="loading-spinner-icon"></div>
<div class="loading-spinner-text">${message}</div>
</div>
`;
document.body.appendChild(overlay);
}
function hideLoading() {
const overlay = document.getElementById("loadingOverlay");
if (overlay) {
overlay.remove();
}
}
// Test functions
function testSuccess() {
showNotification(
"success",
"Success!",
"User has been saved successfully"
);
}
function testError() {
showNotification(
"error",
"Error",
"Failed to save user. Please try again."
);
}
function testLoading() {
showLoading("Saving user...");
setTimeout(() => {
hideLoading();
showNotification(
"success",
"Complete!",
"Operation finished successfully"
);
}, 2000);
}
function testMultiple() {
showNotification("success", "User Created", "John Doe has been added");
setTimeout(() => {
showNotification(
"success",
"Email Sent",
"Welcome email has been sent"
);
}, 500);
setTimeout(() => {
showNotification(
"success",
"Permissions Set",
"User permissions configured"
);
}, 1000);
}
</script>
</body>
</html>

View File

@@ -69,7 +69,7 @@
<div class="test-section">
<h3>Test 3: Navigate to Products Page</h3>
<a href="/admin/products.html" class="btn btn-primary">
<a href="/admin/products" class="btn btn-primary">
🛍️ Go to Products Management
</a>
</div>

View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>User Management API Tests</title>
<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 40px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.test-section {
margin: 30px 0;
padding: 20px;
background: #f9f9f9;
border-left: 4px solid #667eea;
border-radius: 5px;
}
.test-section h3 {
margin-top: 0;
color: #667eea;
}
button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin: 5px;
}
button:hover {
background: #5568d3;
}
.result {
margin-top: 15px;
padding: 15px;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
font-family: "Courier New", monospace;
font-size: 13px;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.success {
border-left: 4px solid #10b981;
background: #f0fdf4;
}
.error {
border-left: 4px solid #ef4444;
background: #fef2f2;
color: #dc2626;
}
.info {
background: #eff6ff;
border-left: 4px solid #3b82f6;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 5px;
width: 200px;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 User Management API Tests</h1>
<div class="info">
<strong>Note:</strong> You must be logged in as an admin to run these
tests. Open the browser console (F12) for detailed logs.
</div>
<!-- Test 1: List All Users -->
<div class="test-section">
<h3>1. List All Users (GET /api/admin/users)</h3>
<button onclick="testListUsers()">Run Test</button>
<div id="result1" class="result" style="display: none"></div>
</div>
<!-- Test 2: Get Single User -->
<div class="test-section">
<h3>2. Get Single User (GET /api/admin/users/:id)</h3>
<input type="text" id="getUserId" placeholder="Enter user ID" />
<button onclick="testGetUser()">Run Test</button>
<div id="result2" class="result" style="display: none"></div>
</div>
<!-- Test 3: Create New User -->
<div class="test-section">
<h3>3. Create New User (POST /api/admin/users)</h3>
<div>
<input
type="text"
id="createName"
placeholder="Name"
value="Test User"
/>
<input type="text" id="createUsername" placeholder="Username" />
<input type="email" id="createEmail" placeholder="Email" />
</div>
<div>
<input
type="password"
id="createPassword"
placeholder="Password"
value="SecurePass123"
/>
<select id="createRole" style="padding: 8px; margin: 5px">
<option value="Cashier">Cashier</option>
<option value="Accountant">Accountant</option>
<option value="Admin">Admin</option>
</select>
</div>
<button onclick="testCreateUser()">Run Test</button>
<div id="result3" class="result" style="display: none"></div>
</div>
<!-- Test 4: Update User -->
<div class="test-section">
<h3>4. Update User (PUT /api/admin/users/:id)</h3>
<input type="text" id="updateUserId" placeholder="User ID" />
<input type="text" id="updateName" placeholder="New Name" />
<select id="updateRole" style="padding: 8px; margin: 5px">
<option value="Cashier">Cashier</option>
<option value="Accountant">Accountant</option>
<option value="Admin">Admin</option>
</select>
<button onclick="testUpdateUser()">Run Test</button>
<div id="result4" class="result" style="display: none"></div>
</div>
<!-- Test 5: Change Password -->
<div class="test-section">
<h3>5. Change Password (PUT /api/admin/users/:id/password)</h3>
<input type="text" id="passwordUserId" placeholder="User ID" />
<input
type="password"
id="newPassword"
placeholder="New Password"
value="NewSecure456"
/>
<button onclick="testChangePassword()">Run Test</button>
<div id="result5" class="result" style="display: none"></div>
</div>
<!-- Test 6: Delete User -->
<div class="test-section">
<h3>6. Delete User (DELETE /api/admin/users/:id)</h3>
<input type="text" id="deleteUserId" placeholder="User ID" />
<button onclick="testDeleteUser()" style="background: #ef4444">
Run Test
</button>
<div id="result6" class="result" style="display: none"></div>
</div>
</div>
<script>
// Generate random username/email
function generateTestCredentials() {
const timestamp = Date.now();
document.getElementById("createUsername").value =
"testuser_" + timestamp;
document.getElementById("createEmail").value =
"test_" + timestamp + "@example.com";
}
// Initialize with random credentials
generateTestCredentials();
// Helper to display results
function showResult(elementId, data, isError = false) {
const resultDiv = document.getElementById(elementId);
resultDiv.style.display = "block";
resultDiv.className = "result " + (isError ? "error" : "success");
resultDiv.textContent =
typeof data === "string" ? data : JSON.stringify(data, null, 2);
}
// Test 1: List all users
async function testListUsers() {
try {
const response = await fetch("/api/admin/users", {
credentials: "include",
});
const data = await response.json();
console.log("List Users:", data);
showResult("result1", data);
} catch (error) {
console.error("Error:", error);
showResult("result1", error.message, true);
}
}
// Test 2: Get single user
async function testGetUser() {
const userId = document.getElementById("getUserId").value;
if (!userId) {
alert("Please enter a user ID");
return;
}
try {
const response = await fetch(`/api/admin/users/${userId}`, {
credentials: "include",
});
const data = await response.json();
console.log("Get User:", data);
showResult("result2", data);
} catch (error) {
console.error("Error:", error);
showResult("result2", error.message, true);
}
}
// Test 3: Create user
async function testCreateUser() {
const userData = {
name: document.getElementById("createName").value,
username: document.getElementById("createUsername").value,
email: document.getElementById("createEmail").value,
password: document.getElementById("createPassword").value,
role: document.getElementById("createRole").value,
isactive: true,
passwordneverexpires: false,
};
try {
const response = await fetch("/api/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(userData),
});
const data = await response.json();
console.log("Create User:", data);
showResult("result3", data);
// Store the created user ID for other tests
if (data.success && data.user) {
document.getElementById("getUserId").value = data.user.id;
document.getElementById("updateUserId").value = data.user.id;
document.getElementById("passwordUserId").value = data.user.id;
document.getElementById("deleteUserId").value = data.user.id;
}
// Generate new credentials for next test
generateTestCredentials();
} catch (error) {
console.error("Error:", error);
showResult("result3", error.message, true);
}
}
// Test 4: Update user
async function testUpdateUser() {
const userId = document.getElementById("updateUserId").value;
if (!userId) {
alert("Please enter a user ID");
return;
}
const updateData = {
name: document.getElementById("updateName").value,
role: document.getElementById("updateRole").value,
};
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(updateData),
});
const data = await response.json();
console.log("Update User:", data);
showResult("result4", data);
} catch (error) {
console.error("Error:", error);
showResult("result4", error.message, true);
}
}
// Test 5: Change password
async function testChangePassword() {
const userId = document.getElementById("passwordUserId").value;
const password = document.getElementById("newPassword").value;
if (!userId) {
alert("Please enter a user ID");
return;
}
try {
const response = await fetch(`/api/admin/users/${userId}/password`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ password: password }),
});
const data = await response.json();
console.log("Change Password:", data);
showResult("result5", data);
} catch (error) {
console.error("Error:", error);
showResult("result5", error.message, true);
}
}
// Test 6: Delete user
async function testDeleteUser() {
const userId = document.getElementById("deleteUserId").value;
if (!userId) {
alert("Please enter a user ID");
return;
}
if (!confirm("Are you sure you want to delete this user?")) {
return;
}
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
console.log("Delete User:", data);
showResult("result6", data);
} catch (error) {
console.error("Error:", error);
showResult("result6", error.message, true);
}
}
</script>
</body>
</html>

View File

@@ -58,6 +58,176 @@
border-color: #667eea;
background: #f0f3ff;
}
/* Custom Notification Styles */
#notificationContainer {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
}
.notification {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
margin-bottom: 12px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: white;
border-left: 4px solid;
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s;
opacity: 1;
transform: translateX(0);
min-width: 320px;
}
.notification.removing {
animation: slideOut 0.3s ease-in;
}
.notification-success {
border-left-color: #10b981;
background: linear-gradient(to right, #f0fdf4 0%, #ffffff 100%);
}
.notification-error {
border-left-color: #ef4444;
background: linear-gradient(to right, #fef2f2 0%, #ffffff 100%);
}
.notification-icon {
font-size: 24px;
flex-shrink: 0;
}
.notification-success .notification-icon {
color: #10b981;
}
.notification-error .notification-icon {
color: #ef4444;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
color: #111;
}
.notification-message {
font-size: 13px;
color: #666;
line-height: 1.4;
}
.notification-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
flex-shrink: 0;
}
.notification-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #333;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0.7;
}
}
/* Loading Spinner Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
backdrop-filter: blur(2px);
}
.loading-spinner-container {
background: white;
padding: 30px 40px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
text-align: center;
}
.loading-spinner-icon {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
.loading-spinner-text {
color: #333;
font-size: 16px;
font-weight: 500;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
@@ -65,44 +235,44 @@
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard.html"
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage.html"
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products.html"><i class="bi bi-box"></i> Products</a>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio.html"
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog.html"><i class="bi bi-newspaper"></i> Blog</a>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages.html"
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library.html"
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu.html"><i class="bi bi-list"></i> Menu</a>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings.html"><i class="bi bi-gear"></i> Settings</a>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users.html" class="active"
<a href="/admin/users" class="active"
><i class="bi bi-people"></i> Users</a
>
</li>
@@ -382,6 +552,9 @@
</div>
</div>
<!-- Custom Notification Container -->
<div id="notificationContainer"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/users.js"></script>

View File

@@ -1,467 +0,0 @@
/* ================================================
MODERN DESIGN SYSTEM - Sky Art Shop
Inspired by leading ecommerce platforms
================================================ */
:root {
/* Primary Color Palette */
--primary: #FF6B6B;
--primary-dark: #EE5A52;
--primary-light: #FF9999;
--secondary: #4ECDC4;
--accent: #FFE66D;
/* Neutral Colors */
--text-primary: #2D3436;
--text-secondary: #636E72;
--text-muted: #B2BEC3;
--bg-primary: #FFFFFF;
--bg-secondary: #F8F9FA;
--bg-tertiary: #E9ECEF;
--border-color: #E1E8ED;
/* Status Colors */
--success: #00B894;
--warning: #FDCB6E;
--error: #D63031;
--info: #74B9FF;
/* Spacing System (8px base) */
--space-xs: 0.5rem; /* 8px */
--space-sm: 1rem; /* 16px */
--space-md: 1.5rem; /* 24px */
--space-lg: 2rem; /* 32px */
--space-xl: 3rem; /* 48px */
--space-2xl: 4rem; /* 64px */
--space-3xl: 6rem; /* 96px */
/* Typography */
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-display: 'Poppins', sans-serif;
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 2rem; /* 32px */
--font-size-4xl: 2.5rem; /* 40px */
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
--shadow-2xl: 0 25px 50px rgba(0, 0, 0, 0.25);
/* Border Radius */
--radius-sm: 0.375rem; /* 6px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 0.75rem; /* 12px */
--radius-xl: 1rem; /* 16px */
--radius-2xl: 1.5rem; /* 24px */
--radius-full: 9999px;
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
/* Z-index layers */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* ================================================
RESET & BASE STYLES
================================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-primary);
color: var(--text-primary);
background-color: var(--bg-primary);
line-height: 1.6;
overflow-x: hidden;
}
/* ================================================
TYPOGRAPHY
================================================ */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 600;
line-height: 1.2;
color: var(--text-primary);
}
h1 { font-size: var(--font-size-4xl); margin-bottom: var(--space-lg); }
h2 { font-size: var(--font-size-3xl); margin-bottom: var(--space-md); }
h3 { font-size: var(--font-size-2xl); margin-bottom: var(--space-md); }
h4 { font-size: var(--font-size-xl); margin-bottom: var(--space-sm); }
h5 { font-size: var(--font-size-lg); margin-bottom: var(--space-sm); }
h6 { font-size: var(--font-size-base); margin-bottom: var(--space-sm); }
p {
margin-bottom: var(--space-sm);
color: var(--text-secondary);
}
a {
color: var(--primary);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--primary-dark);
}
/* ================================================
CONTAINER & LAYOUT
================================================ */
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--space-lg);
}
.container-fluid {
width: 100%;
padding: 0 var(--space-lg);
}
.section {
padding: var(--space-3xl) 0;
}
.section-sm {
padding: var(--space-2xl) 0;
}
/* Grid System */
.grid {
display: grid;
gap: var(--space-lg);
}
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.grid-cols-5 { grid-template-columns: repeat(5, 1fr); }
/* Flexbox Utilities */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-sm { gap: var(--space-sm); }
.gap-md { gap: var(--space-md); }
.gap-lg { gap: var(--space-lg); }
/* ================================================
BUTTONS
================================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-lg);
font-family: var(--font-primary);
font-size: var(--font-size-base);
font-weight: 500;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-base);
text-decoration: none;
white-space: nowrap;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: var(--shadow-md);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
color: white;
}
.btn-secondary {
background: var(--secondary);
color: white;
}
.btn-outline {
background: transparent;
border: 2px solid var(--primary);
color: var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn-ghost {
background: transparent;
color: var(--text-primary);
}
.btn-ghost:hover {
background: var(--bg-secondary);
}
.btn-sm {
padding: var(--space-xs) var(--space-md);
font-size: var(--font-size-sm);
}
.btn-lg {
padding: var(--space-md) var(--space-xl);
font-size: var(--font-size-lg);
}
.btn-icon {
padding: var(--space-sm);
border-radius: var(--radius-full);
}
/* ================================================
CARDS
================================================ */
.card {
background: var(--bg-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: all var(--transition-base);
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-4px);
}
.card-body {
padding: var(--space-lg);
}
/* ================================================
BADGES
================================================ */
.badge {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-xs);
font-weight: 600;
border-radius: var(--radius-full);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-primary { background: var(--primary-light); color: var(--primary-dark); }
.badge-success { background: #C6F6D5; color: #22543D; }
.badge-warning { background: #FEF3C7; color: #92400E; }
.badge-error { background: #FED7D7; color: #742A2A; }
.badge-info { background: #DBEAFE; color: #1E3A8A; }
/* ================================================
FORMS
================================================ */
.form-group {
margin-bottom: var(--space-md);
}
.form-label {
display: block;
margin-bottom: var(--space-xs);
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--text-primary);
}
.form-control {
width: 100%;
padding: var(--space-sm) var(--space-md);
font-family: var(--font-primary);
font-size: var(--font-size-base);
color: var(--text-primary);
background: var(--bg-primary);
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.1);
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23636E72' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--space-sm) center;
padding-right: var(--space-xl);
}
/* ================================================
FOOTER
================================================ */
.footer {
background: var(--text-primary);
color: white;
padding: var(--space-3xl) 0 var(--space-lg);
margin-top: var(--space-3xl);
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: var(--space-xl);
margin-bottom: var(--space-2xl);
}
.footer-col {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.footer-title {
font-size: var(--font-size-2xl);
font-weight: 700;
margin-bottom: var(--space-sm);
}
.footer-text {
color: var(--text-muted);
line-height: 1.6;
}
.footer-heading {
font-size: var(--font-size-base);
font-weight: 600;
margin-bottom: var(--space-sm);
}
.footer-links {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.footer-links a {
color: rgba(255, 255, 255, 0.7);
transition: color var(--transition-fast);
}
.footer-links a:hover {
color: white;
}
.footer-bottom {
padding-top: var(--space-lg);
border-top: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
color: rgba(255, 255, 255, 0.6);
}
.social-links {
display: flex;
gap: var(--space-sm);
}
.social-link {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: var(--radius-full);
color: white;
transition: all var(--transition-fast);
}
.social-link:hover {
background: var(--primary);
transform: translateY(-2px);
}
/* ================================================
RESPONSIVE
================================================ */
@media (max-width: 1024px) {
.grid-cols-5 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(3, 1fr); }
.footer-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
html { font-size: 14px; }
.container { padding: 0 var(--space-md); }
.grid-cols-5,
.grid-cols-4,
.grid-cols-3 { grid-template-columns: repeat(2, 1fr); }
.section { padding: var(--space-2xl) 0; }
h1 { font-size: var(--font-size-3xl); }
h2 { font-size: var(--font-size-2xl); }
.footer-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.grid-cols-5,
.grid-cols-4,
.grid-cols-3,
.grid-cols-2 { grid-template-columns: 1fr; }
.btn { width: 100%; }
}

View File

@@ -391,136 +391,10 @@ p {
}
/* ====================================
Navigation Bar
Navigation Bar - See navbar.css for modern navbar styles
==================================== */
.navbar {
background-color: var(--bg-color);
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 1000;
}
.navbar-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
position: relative;
}
.nav-brand {
flex-shrink: 0;
}
.nav-brand a {
text-decoration: none;
display: flex;
align-items: center;
gap: 12px;
}
.logo-image {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 50%;
}
.nav-brand h1 {
font-size: 1.8rem;
color: var(--primary-color);
margin: 0;
}
.nav-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
pointer-events: none;
}
.nav-center .nav-menu {
pointer-events: auto;
}
.nav-menu {
display: flex;
gap: 2rem;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.nav-menu li {
margin: 0;
padding: 0;
}
.nav-menu a {
color: var(--text-color);
font-weight: 500;
transition: var(--transition);
position: relative;
white-space: nowrap;
text-decoration: none;
padding: 0.5rem 0;
display: block;
}
.nav-menu a:hover,
.nav-menu a.active {
color: var(--primary-color);
}
.nav-menu a.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: var(--primary-color);
}
.nav-icons {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.nav-icon {
position: relative;
color: var(--text-color);
font-size: 1.5rem;
transition: var(--transition);
text-decoration: none;
display: flex;
align-items: center;
}
.nav-icon:hover {
color: var(--primary-color);
}
.nav-icon .badge {
position: absolute;
top: -8px;
right: -8px;
background-color: var(--primary-color);
color: white;
font-size: 0.7rem;
font-weight: 600;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
line-height: 1;
display: none;
}
/* Old navbar styles removed to prevent conflicts with modern-navbar */
/* All navbar styling is now in navbar.css */
/* Cart and Wishlist Dropdown */
.dropdown-container {
@@ -708,27 +582,7 @@ p {
text-decoration: none !important;
}
.nav-toggle {
display: flex;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 10px;
flex-shrink: 0;
}
.nav-toggle span {
width: 25px;
height: 3px;
background-color: var(--text-color);
transition: var(--transition);
}
.nav-toggle:hover span {
background-color: var(--primary-color);
}
/* Old nav-toggle styles removed - now in navbar.css */
.nav-dropdown {
display: none;
@@ -785,8 +639,10 @@ p {
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
align-items: center;
padding: var(--spacing-xl) 0;
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
padding: var(--spacing-xl) 4rem;
background: #FFEBEB;
max-width: 1400px;
margin: 0 auto;
}
.hero-content {
@@ -833,7 +689,7 @@ p {
position: relative;
overflow: hidden;
border-radius: 10px;
padding-right: 2rem;
padding-right: 0;
}
.hero-image img {
@@ -1056,21 +912,25 @@ section {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow-sm);
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.15);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--border-color);
border: 1px solid #FFD0D0;
display: flex;
flex-direction: column;
height: 100%;
}
.product-card:hover {
box-shadow: var(--shadow-hover);
box-shadow: 0 4px 16px rgba(252, 177, 216, 0.25);
transform: translateY(-8px);
border-color: var(--primary-color);
border-color: #FCB1D8;
}
.product-image {
position: relative;
overflow: hidden;
height: 180px;
width: 100%;
aspect-ratio: 1;
}
.product-image img {
@@ -1084,30 +944,47 @@ section {
transform: scale(1.1);
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 8px;
}
.product-card h3 {
padding: var(--spacing-sm);
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #202023;
line-height: 1.4;
}
.product-color-badge {
display: inline-block;
margin: 0 var(--spacing-sm) var(--spacing-xs);
margin: 8px 0;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
background: var(--primary-color);
color: white;
background: #FCB1D8;
color: #202023;
border-radius: 12px;
letter-spacing: 0.5px;
}
.product-description {
padding: 0 var(--spacing-sm);
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: var(--spacing-xs);
color: #202023;
opacity: 0.7;
margin: 0;
line-height: 1.6;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Product Description Rich Text Styles */
@@ -1141,14 +1018,47 @@ section {
}
.price {
padding: 0 var(--spacing-sm);
font-size: 1.3rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: var(--spacing-sm);
color: #FCB1D8;
margin: 0;
}
.product-card .btn {
.product-actions {
display: flex;
gap: 8px;
padding: 0 16px 16px 16px;
margin-top: auto;
}
.product-actions .btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: #FCB1D8;
color: #202023;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin: 0;
width: auto;
}
.product-actions .btn:hover {
background: #F6CCDE;
transform: translateY(-2px);
}
.product-actions .btn i {
font-size: 18px;
}
.product-card .btn:not(.product-actions .btn) {
width: calc(100% - var(--spacing-md));
margin: 0 var(--spacing-sm) var(--spacing-sm);
}
@@ -1858,6 +1768,7 @@ section {
.hero {
grid-template-columns: 1fr;
padding: var(--spacing-xl) 2rem;
}
.hero-content h2 {
@@ -3189,6 +3100,32 @@ section {
}
.product-card .product-link:hover h3 {
color: var(--primary-color);
color: #FCB1D8;
}
/* Product Title Link - Make entire title clickable */
.product-title-link {
text-decoration: none;
color: inherit;
display: block;
cursor: pointer;
transition: color 0.3s ease;
}
.product-title-link:hover {
color: #FCB1D8;
}
.product-title-link h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: inherit;
line-height: 1.4;
transition: color 0.3s ease;
}
.product-title-link:hover h3 {
color: #FCB1D8;
}

View File

@@ -1,464 +0,0 @@
/* ================================================
MODERN NAVIGATION - Ecommerce Style
================================================ */
.modern-nav {
position: sticky;
top: 0;
background: white;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
z-index: var(--z-sticky);
transition: all var(--transition-base);
}
/* Top Bar (Promo/Announcement) */
.nav-topbar {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: var(--space-xs) 0;
font-size: var(--font-size-sm);
text-align: center;
}
.nav-topbar a {
color: white;
text-decoration: underline;
font-weight: 600;
}
/* Main Navigation */
.nav-main {
padding: var(--space-md) 0;
}
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-xl);
}
/* Logo */
.nav-logo {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
}
.nav-logo-image {
height: 40px;
width: auto;
}
.nav-logo-text {
font-family: var(--font-display);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Search Bar */
.nav-search {
flex: 1;
max-width: 600px;
position: relative;
}
.search-input-wrapper {
position: relative;
}
.search-input {
width: 100%;
padding: var(--space-sm) var(--space-xl) var(--space-sm) var(--space-lg);
border: 2px solid var(--border-color);
border-radius: var(--radius-full);
font-size: var(--font-size-base);
transition: all var(--transition-fast);
}
.search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.1);
}
.search-icon {
position: absolute;
left: var(--space-md);
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
.search-btn {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
padding: var(--space-xs) var(--space-lg);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius-full);
cursor: pointer;
font-weight: 600;
transition: all var(--transition-fast);
}
.search-btn:hover {
background: var(--primary-dark);
}
/* Nav Actions */
.nav-actions {
display: flex;
align-items: center;
gap: var(--space-md);
}
.nav-icon-btn {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: var(--space-xs);
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
border-radius: var(--radius-md);
}
.nav-icon-btn:hover {
background: var(--bg-secondary);
color: var(--primary);
}
.nav-icon-btn i {
font-size: 24px;
}
.nav-icon-label {
font-size: var(--font-size-xs);
font-weight: 500;
}
.nav-badge {
position: absolute;
top: 0;
right: 0;
min-width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
background: var(--error);
color: white;
font-size: 10px;
font-weight: 700;
border-radius: var(--radius-full);
border: 2px solid white;
}
/* Nav Links */
.nav-links-wrapper {
border-top: 1px solid var(--border-color);
padding: var(--space-sm) 0;
}
.nav-links {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-xl);
list-style: none;
}
.nav-link {
position: relative;
padding: var(--space-xs) 0;
font-size: var(--font-size-base);
font-weight: 500;
color: var(--text-primary);
text-decoration: none;
transition: color var(--transition-fast);
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--primary);
transform: scaleX(0);
transition: transform var(--transition-base);
}
.nav-link:hover {
color: var(--primary);
}
.nav-link:hover::after,
.nav-link.active::after {
transform: scaleX(1);
}
/* Mobile Menu */
.mobile-menu-btn {
display: none;
padding: var(--space-sm);
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
font-size: 24px;
}
.mobile-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: var(--z-modal-backdrop);
opacity: 0;
transition: opacity var(--transition-base);
}
.mobile-overlay.active {
opacity: 1;
}
.mobile-menu {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 320px;
max-width: 90%;
background: white;
z-index: var(--z-modal);
transform: translateX(100%);
transition: transform var(--transition-base);
overflow-y: auto;
}
.mobile-menu.active {
transform: translateX(0);
}
.mobile-menu-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg);
border-bottom: 1px solid var(--border-color);
}
.mobile-menu-title {
font-size: var(--font-size-lg);
font-weight: 600;
}
.mobile-close-btn {
padding: var(--space-xs);
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
font-size: 24px;
}
.mobile-menu-content {
padding: var(--space-lg);
}
.mobile-nav-links {
display: flex;
flex-direction: column;
gap: var(--space-sm);
list-style: none;
margin-bottom: var(--space-xl);
}
.mobile-nav-link {
padding: var(--space-sm);
color: var(--text-primary);
text-decoration: none;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
font-weight: 500;
}
.mobile-nav-link:hover {
background: var(--bg-secondary);
color: var(--primary);
}
/* Dropdown Menus */
.nav-dropdown {
position: relative;
}
.dropdown-content {
position: absolute;
top: 100%;
left: 0;
min-width: 280px;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
padding: var(--space-md);
opacity: 0;
visibility: hidden;
transform: translateY(10px);
transition: all var(--transition-base);
z-index: var(--z-dropdown);
}
.nav-dropdown:hover .dropdown-content {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-items {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.dropdown-item {
padding: var(--space-sm) var(--space-md);
color: var(--text-primary);
text-decoration: none;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
display: flex;
align-items: center;
gap: var(--space-sm);
}
.dropdown-item:hover {
background: var(--bg-secondary);
color: var(--primary);
}
/* Cart Dropdown */
.cart-dropdown {
min-width: 360px;
right: 0;
left: auto;
}
.cart-dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--border-color);
}
.cart-items {
max-height: 300px;
overflow-y: auto;
margin-bottom: var(--space-md);
}
.cart-item {
display: flex;
gap: var(--space-sm);
padding: var(--space-sm);
border-radius: var(--radius-md);
transition: background var(--transition-fast);
}
.cart-item:hover {
background: var(--bg-secondary);
}
.cart-item-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: var(--radius-md);
}
.cart-item-info {
flex: 1;
}
.cart-item-name {
font-size: var(--font-size-sm);
font-weight: 600;
margin-bottom: 2px;
}
.cart-item-price {
font-size: var(--font-size-sm);
color: var(--primary);
font-weight: 600;
}
.cart-dropdown-footer {
padding-top: var(--space-md);
border-top: 1px solid var(--border-color);
}
.cart-total {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-md);
font-weight: 600;
}
/* Responsive */
@media (max-width: 1024px) {
.nav-search {
max-width: 400px;
}
.nav-links-wrapper {
display: none;
}
}
@media (max-width: 768px) {
.nav-search {
display: none;
}
.nav-icon-label {
display: none;
}
.mobile-menu-btn,
.mobile-overlay,
.mobile-menu {
display: block;
}
.nav-actions {
gap: var(--space-sm);
}
.nav-icon-btn {
padding: var(--space-xs);
}
}

View File

@@ -1,590 +0,0 @@
/* ================================================
MODERN SHOP PAGE - Ecommerce Style
================================================ */
/* Hero Banner */
.shop-hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: var(--space-3xl) 0 var(--space-2xl);
color: white;
text-align: center;
position: relative;
overflow: hidden;
}
.shop-hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.1;
}
.shop-hero-content {
position: relative;
z-index: 1;
}
.shop-hero h1 {
color: white;
font-size: var(--font-size-4xl);
font-weight: 700;
margin-bottom: var(--space-sm);
}
.shop-hero p {
color: rgba(255, 255, 255, 0.9);
font-size: var(--font-size-lg);
margin-bottom: 0;
}
/* Categories Carousel */
.categories-section {
padding: var(--space-xl) 0;
background: var(--bg-secondary);
}
.categories-scroll {
display: flex;
gap: var(--space-md);
overflow-x: auto;
scroll-behavior: smooth;
padding: var(--space-sm) 0;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.categories-scroll::-webkit-scrollbar {
display: none;
}
.category-chip {
flex-shrink: 0;
padding: var(--space-sm) var(--space-lg);
background: white;
border: 2px solid var(--border-color);
border-radius: var(--radius-full);
font-weight: 500;
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.category-chip:hover,
.category-chip.active {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: translateY(-2px);
}
/* Shop Layout */
.shop-container {
padding: var(--space-2xl) 0;
}
.shop-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: var(--space-xl);
}
/* Sidebar Filters */
.shop-sidebar {
position: sticky;
top: 100px;
height: fit-content;
background: white;
border-radius: var(--radius-lg);
padding: var(--space-lg);
box-shadow: var(--shadow-sm);
}
.filter-section {
margin-bottom: var(--space-xl);
}
.filter-section:last-child {
margin-bottom: 0;
}
.filter-title {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--space-md);
color: var(--text-primary);
}
.filter-group {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.filter-option {
display: flex;
align-items: center;
gap: var(--space-sm);
cursor: pointer;
padding: var(--space-xs);
border-radius: var(--radius-sm);
transition: background var(--transition-fast);
}
.filter-option:hover {
background: var(--bg-secondary);
}
.filter-option input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--primary);
}
.filter-option label {
flex: 1;
cursor: pointer;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.filter-count {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
/* Price Range Slider */
.price-range {
padding: var(--space-md) 0;
}
.price-inputs {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-md);
}
.price-input {
flex: 1;
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
/* Shop Main Content */
.shop-main {
min-width: 0;
}
/* Toolbar */
.shop-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-xl);
padding: var(--space-md) var(--space-lg);
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-wrap: wrap;
gap: var(--space-md);
}
.shop-results {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.shop-results strong {
color: var(--text-primary);
font-weight: 600;
}
.shop-controls {
display: flex;
align-items: center;
gap: var(--space-md);
}
.view-toggle {
display: flex;
gap: var(--space-xs);
}
.view-btn {
padding: var(--space-xs) var(--space-sm);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.view-btn.active {
background: var(--primary);
color: white;
}
.sort-select {
padding: var(--space-xs) var(--space-lg) var(--space-xs) var(--space-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
cursor: pointer;
background: white;
}
/* Products Grid */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-lg);
}
/* Product Card */
.product-card {
background: white;
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all var(--transition-base);
position: relative;
display: flex;
flex-direction: column;
}
.product-card:hover {
box-shadow: var(--shadow-xl);
transform: translateY(-8px);
}
.product-image-wrapper {
position: relative;
overflow: hidden;
background: var(--bg-secondary);
aspect-ratio: 1;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--transition-slow);
}
.product-card:hover .product-image {
transform: scale(1.1);
}
.product-badges {
position: absolute;
top: var(--space-sm);
left: var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-xs);
z-index: 2;
}
.product-badge {
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-xs);
font-weight: 700;
border-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: var(--shadow-md);
}
.badge-new {
background: var(--secondary);
color: white;
}
.badge-sale {
background: var(--error);
color: white;
}
.badge-bestseller {
background: var(--accent);
color: var(--text-primary);
}
.product-actions {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-xs);
opacity: 0;
transform: translateX(10px);
transition: all var(--transition-base);
}
.product-card:hover .product-actions {
opacity: 1;
transform: translateX(0);
}
.product-action-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border: none;
border-radius: var(--radius-full);
cursor: pointer;
box-shadow: var(--shadow-md);
transition: all var(--transition-fast);
color: var(--text-primary);
}
.product-action-btn:hover {
background: var(--primary);
color: white;
transform: scale(1.1);
}
.product-action-btn.active {
background: var(--error);
color: white;
}
.product-info {
padding: var(--space-md);
flex: 1;
display: flex;
flex-direction: column;
}
.product-category {
font-size: var(--font-size-xs);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: var(--space-xs);
}
.product-title {
font-size: var(--font-size-base);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-xs);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-rating {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-bottom: var(--space-sm);
}
.stars {
display: flex;
gap: 2px;
color: var(--accent);
}
.rating-count {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.product-price {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.price-current {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--primary);
}
.price-original {
font-size: var(--font-size-base);
color: var(--text-muted);
text-decoration: line-through;
}
.price-discount {
padding: 2px var(--space-xs);
background: var(--error);
color: white;
font-size: var(--font-size-xs);
font-weight: 700;
border-radius: var(--radius-sm);
}
.product-footer {
display: flex;
gap: var(--space-xs);
}
.add-to-cart-btn {
flex: 1;
padding: var(--space-sm);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
}
.add-to-cart-btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.quick-view-btn {
padding: var(--space-sm);
background: var(--bg-secondary);
color: var(--text-primary);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
}
.quick-view-btn:hover {
background: var(--text-primary);
color: white;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: var(--space-sm);
margin-top: var(--space-2xl);
}
.page-btn {
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
}
.page-btn:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.page-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* Mobile Filter Toggle */
.mobile-filter-btn {
display: none;
width: 100%;
padding: var(--space-md);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius-lg);
font-weight: 600;
cursor: pointer;
margin-bottom: var(--space-lg);
}
/* Responsive */
@media (max-width: 1024px) {
.shop-layout {
grid-template-columns: 1fr;
}
.shop-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 320px;
max-width: 90%;
transform: translateX(-100%);
transition: transform var(--transition-base);
z-index: var(--z-modal);
overflow-y: auto;
}
.shop-sidebar.active {
transform: translateX(0);
}
.mobile-filter-btn {
display: block;
}
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (max-width: 640px) {
.shop-hero {
padding: var(--space-2xl) 0;
}
.shop-hero h1 {
font-size: var(--font-size-2xl);
}
.shop-toolbar {
flex-direction: column;
align-items: stretch;
}
.shop-controls {
justify-content: space-between;
}
.products-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-md);
}
.product-info {
padding: var(--space-sm);
}
.product-title {
font-size: var(--font-size-sm);
}
.price-current {
font-size: var(--font-size-lg);
}
}

View File

@@ -1,10 +1,19 @@
/* Import Amsterdam Three Font */
@font-face {
font-family: 'Amsterdam Three';
src: url('/assets/fonts/AmsterdamThreeSlant-axaym.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Modern Navbar Styles */
.modern-navbar {
position: sticky;
top: 0;
z-index: 1000;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background: #FFD0D0;
box-shadow: none;
font-family: 'Roboto', sans-serif;
}
@@ -12,21 +21,23 @@
max-width: 1400px;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
height: 72px;
}
/* Logo Section */
.navbar-brand {
flex-shrink: 0;
flex-shrink: 0 !important;
min-width: 240px !important;
margin-right: 48px !important;
}
.brand-link {
display: flex;
align-items: center;
gap: 12px;
display: flex !important;
align-items: center !important;
gap: 20px !important;
text-decoration: none;
transition: opacity 0.2s;
}
@@ -36,32 +47,34 @@
}
.brand-logo {
width: 48px;
height: 48px;
width: 56px;
height: 56px;
object-fit: contain;
border-radius: 8px;
}
.brand-name {
font-family: 'Amsterdam Three', cursive;
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
letter-spacing: 0.3px;
font-weight: 400;
color: #202023;
letter-spacing: 0.5px;
white-space: nowrap;
}
/* Main Navigation */
.navbar-menu {
flex: 1;
display: flex;
justify-content: center;
padding: 0 32px;
flex: 1 !important;
display: flex !important;
justify-content: center !important;
padding: 0 60px !important;
min-width: 0 !important;
}
.nav-menu-list {
display: flex;
align-items: center;
gap: 8px;
display: flex !important;
align-items: center !important;
gap: 8px !important;
list-style: none;
margin: 0;
padding: 0;
@@ -76,7 +89,7 @@
padding: 10px 20px;
font-size: 15px;
font-weight: 500;
color: #4a4a4a;
color: #202023;
text-decoration: none;
border-radius: 6px;
transition: all 0.2s;
@@ -85,16 +98,19 @@
.nav-link:hover,
.nav-link.active {
color: #6b46c1;
background: #f3f0ff;
color: #202023;
background: #FCB1D8;
}
/* Right Actions */
.navbar-actions {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
display: flex !important;
align-items: center !important;
gap: 16px !important;
flex-shrink: 0 !important;
min-width: 120px !important;
justify-content: flex-end !important;
margin-left: 48px !important;
}
.action-item {
@@ -110,7 +126,7 @@
height: 44px;
border: none;
background: transparent;
color: #4a4a4a;
color: #202023;
font-size: 22px;
border-radius: 50%;
cursor: pointer;
@@ -118,8 +134,8 @@
}
.action-btn:hover {
background: #f5f5f5;
color: #6b46c1;
background: #FFEBEB;
color: #202023;
}
.action-badge {
@@ -129,8 +145,8 @@
min-width: 18px;
height: 18px;
padding: 0 5px;
background: #dc2626;
color: white;
background: #FCB1D8;
color: #202023;
font-size: 11px;
font-weight: 600;
border-radius: 9px;
@@ -338,8 +354,9 @@
}
.mobile-brand {
font-size: 18px;
font-weight: 600;
font-family: 'Amsterdam Three', cursive;
font-size: 22px;
font-weight: 400;
color: #1a1a1a;
}
@@ -397,6 +414,15 @@
.mobile-toggle {
display: flex;
}
.navbar-brand {
min-width: auto;
margin-right: auto;
}
.navbar-actions {
margin-left: 16px;
}
}
@media (max-width: 640px) {
@@ -410,8 +436,18 @@
}
.brand-logo {
width: 40px;
height: 40px;
width: 44px;
height: 44px;
}
.navbar-brand {
min-width: auto;
margin-right: 12px;
}
.navbar-actions {
margin-left: 12px;
gap: 8px;
}
.action-dropdown {

View File

@@ -0,0 +1,439 @@
/**
* Enhanced Responsive Utilities
* Comprehensive responsive design system with accessibility
*/
/* ========================================
LOADING STATES
======================================== */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ========================================
PRODUCT GRID RESPONSIVE
======================================== */
.products-grid {
display: grid;
gap: 24px;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
}
@media (min-width: 768px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}
@media (min-width: 1024px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
gap: 28px;
}
}
@media (min-width: 1280px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
}
/* ========================================
PRODUCT CARD RESPONSIVE
======================================== */
.product-card {
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.15);
transition: all 0.3s ease;
}
.product-card:hover {
box-shadow: 0 4px 16px rgba(252, 177, 216, 0.25);
transform: translateY(-4px);
}
.product-image {
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
border-radius: 0;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.product-card:hover .product-image img {
transform: scale(1.05);
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 8px;
}
.product-info h3 {
font-size: 16px;
font-weight: 600;
margin: 0;
line-height: 1.4;
color: #202023;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-description {
font-size: 14px;
color: #202023;
opacity: 0.7;
margin: 0;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex: 1;
min-height: 42px;
}
.product-card .price {
font-size: 20px;
font-weight: 700;
color: #FCB1D8;
margin: 0;
}
.product-actions {
display: flex;
gap: 8px;
padding: 0 16px 16px 16px;
margin-top: auto;
}
.product-actions .btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: #FCB1D8;
color: #202023;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.product-actions .btn:hover {
background: #F6CCDE;
transform: translateY(-2px);
}
.product-actions .btn i {
font-size: 18px;
}
@media (max-width: 639px) {
.product-info h3 {
font-size: 14px;
}
.product-description {
font-size: 13px;
-webkit-line-clamp: 2;
}
.product-card .price {
font-size: 18px;
}
}
/* ========================================
NAVBAR RESPONSIVE
======================================== */
.modern-navbar {
padding: 0 20px;
}
@media (min-width: 768px) {
.modern-navbar {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.modern-navbar {
padding: 0 60px;
}
}
.navbar-brand {
min-width: 200px;
}
@media (max-width: 767px) {
.navbar-brand {
min-width: 150px;
}
.navbar-menu {
display: none;
}
}
/* ========================================
MOBILE MENU
======================================== */
.mobile-menu {
position: fixed;
top: 0;
left: -100%;
width: 280px;
height: 100vh;
background: white;
z-index: 9999;
transition: left 0.3s ease;
overflow-y: auto;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.mobile-menu.active {
left: 0;
}
.mobile-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 9998;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-menu.active ~ .mobile-menu-overlay,
.mobile-menu-overlay.active {
opacity: 1;
visibility: visible;
}
@media (min-width: 768px) {
.mobile-menu-toggle {
display: none;
}
}
/* ========================================
BUTTONS RESPONSIVE
======================================== */
.btn {
padding: 10px 20px;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s;
}
.btn-small {
padding: 8px 16px;
font-size: 13px;
}
.btn-icon {
width: 40px;
height: 40px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
@media (max-width: 639px) {
.btn {
padding: 8px 16px;
font-size: 13px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.btn-icon {
width: 36px;
height: 36px;
}
}
/* ========================================
UTILITY CLASSES
======================================== */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
@media (min-width: 768px) {
.container {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.container {
padding: 0 60px;
}
}
/* Text utilities */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
/* Display utilities */
.hidden { display: none !important; }
.block { display: block !important; }
.inline-block { display: inline-block !important; }
.flex { display: flex !important; }
.inline-flex { display: inline-flex !important; }
/* Responsive visibility */
@media (max-width: 639px) {
.hidden-mobile { display: none !important; }
}
@media (min-width: 640px) and (max-width: 767px) {
.hidden-tablet { display: none !important; }
}
@media (min-width: 768px) {
.hidden-desktop { display: none !important; }
}
@media (max-width: 639px) {
.visible-mobile { display: block !important; }
}
@media (min-width: 640px) and (max-width: 767px) {
.visible-tablet { display: block !important; }
}
@media (min-width: 768px) {
.visible-desktop { display: block !important; }
}
/* ========================================
ACCESSIBILITY
======================================== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border-width: 0;
}
.skip-link {
position: fixed;
top: -100px;
left: 10px;
background: #667eea;
color: white;
padding: 10px 20px;
border-radius: 4px;
text-decoration: none;
z-index: 10001;
transition: top 0.2s;
}
.skip-link:focus {
top: 10px;
outline: 2px solid white;
outline-offset: 2px;
}
*:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* ========================================
PRINT STYLES
======================================== */
@media print {
.modern-navbar,
.mobile-menu,
.notification-container,
.btn,
footer {
display: none !important;
}
body {
font-size: 12pt;
line-height: 1.5;
}
.product-card {
page-break-inside: avoid;
}
}

View File

@@ -0,0 +1,626 @@
/**
* Responsive Layout Utilities
* Mobile-first responsive design system
*/
/* ========================================
RESPONSIVE UTILITIES
======================================== */
/* Loading States */
.loading {
position: relative;
pointer-events: none;
opacity: 0.6;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin: -20px 0 0 -20px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Accessibility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border-width: 0;
}
.focus-visible:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* Responsive Images */
img {
max-width: 100%;
height: auto;
display: block;
}
/* Container Queries */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
@media (min-width: 768px) {
.container {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.container {
padding: 0 60px;
}
}
/* Grid System */
.grid {
display: grid;
gap: 20px;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 768px) {
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1024px) {
.grid-4 {
grid-template-columns: repeat(4, 1fr);
}
}
/* Flex Utilities */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
/* Spacing */
.m-0 { margin: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.p-0 { padding: 0; }
.p-2 { padding: 0.5rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
/* Text Utilities */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-sm { font-size: 0.875rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
/* Display Utilities */
.hidden { display: none !important; }
.block { display: block; }
.inline-block { display: inline-block; }
@media (max-width: 639px) {
.sm\\:hidden { display: none !important; }
}
@media (min-width: 640px) {
.sm\\:block { display: block; }
}
@media (max-width: 767px) {
.md\\:hidden { display: none !important; }
}
@media (min-width: 768px) {
.md\\:block { display: block; }
.md\\:flex { display: flex; }
}
@media (max-width: 1023px) {
.lg\\:hidden { display: none !important; }
}
@media (min-width: 1024px) {
.lg\\:block { display: block; }
.lg\\:flex { display: flex; }
}
/* ========================================
RESPONSIVE PRODUCT CARDS
======================================== */
.products-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
padding: 20px 0;
}
@media (min-width: 640px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
}
@media (min-width: 768px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
gap: 30px;
}
}
@media (min-width: 1024px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.product-image-wrapper {
position: relative;
padding-top: 100%;
overflow: hidden;
background: #f5f5f5;
}
.product-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.product-info {
padding: 16px;
}
.product-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 8px 0;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
font-size: 1.25rem;
font-weight: 700;
color: #667eea;
margin: 0 0 12px 0;
}
.product-actions {
display: flex;
gap: 8px;
flex-direction: column;
}
@media (min-width: 768px) {
.product-actions {
flex-direction: row;
}
}
.wishlist-btn {
position: absolute;
top: 12px;
right: 12px;
width: 40px;
height: 40px;
border-radius: 50%;
background: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #666;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 10;
}
.wishlist-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
.wishlist-btn.active {
background: #dc3545;
color: white;
}
/* ========================================
RESPONSIVE CART/WISHLIST DROPDOWNS
======================================== */
.action-dropdown {
position: absolute;
top: 100%;
right: 0;
width: 100vw;
max-width: 400px;
background: white;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-radius: 12px;
margin-top: 8px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 1000;
max-height: 80vh;
overflow-y: auto;
}
@media (max-width: 639px) {
.action-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
max-width: 100%;
border-radius: 12px 12px 0 0;
transform: translateY(100%);
}
}
.action-dropdown.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dropdown-head h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
}
.dropdown-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
padding: 4px;
line-height: 1;
}
.dropdown-body {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.dropdown-foot {
padding: 16px 20px;
border-top: 1px solid #eee;
}
.cart-item,
.wishlist-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.cart-item:last-child,
.wishlist-item:last-child {
border-bottom: none;
}
.cart-item-image,
.wishlist-item-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.cart-item-details,
.wishlist-item-details {
flex: 1;
}
.cart-item-title,
.wishlist-item-title {
font-size: 0.875rem;
font-weight: 600;
margin: 0 0 4px 0;
}
.cart-item-price,
.wishlist-item-price {
font-size: 0.875rem;
color: #667eea;
font-weight: 600;
margin: 0;
}
.cart-item-remove,
.wishlist-item-remove {
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 4px;
font-size: 16px;
}
.cart-item-remove:hover,
.wishlist-item-remove:hover {
color: #dc3545;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
}
/* ========================================
RESPONSIVE BUTTONS
======================================== */
button,
.btn,
.btn-primary,
.btn-secondary,
.btn-outline {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
border: none;
white-space: nowrap;
}
.btn-primary,
.btn-primary-full {
background: #667eea;
color: white;
width: 100%;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102,126,234,0.3);
}
.btn-secondary {
background: #28a745;
color: white;
}
.btn-secondary:hover {
background: #218838;
}
.btn-outline {
background: white;
color: #667eea;
border: 2px solid #667eea;
width: 100%;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
.btn-text {
background: none;
color: #667eea;
text-decoration: underline;
padding: 8px;
}
@media (max-width: 639px) {
button,
.btn {
font-size: 0.875rem;
padding: 10px 20px;
}
}
/* ========================================
RESPONSIVE NAVIGATION
======================================== */
/* Navbar styles removed - see navbar.css for all navbar styling */
.action-btn {
position: relative;
background: none;
border: none;
font-size: 24px;
color: #333;
cursor: pointer;
padding: 8px;
}
.action-badge {
position: absolute;
top: 0;
right: 0;
background: #dc3545;
color: white;
font-size: 10px;
font-weight: 700;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: none;
align-items: center;
justify-content: center;
padding: 0 4px;
}
/* Mobile Menu */
@media (max-width: 767px) {
.mobile-menu-toggle {
display: block;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 8px;
}
.mobile-menu {
position: fixed;
top: 0;
right: -100%;
width: 80%;
max-width: 300px;
height: 100vh;
background: white;
box-shadow: -4px 0 12px rgba(0,0,0,0.1);
transition: right 0.3s ease;
z-index: 1001;
overflow-y: auto;
}
.mobile-menu.active {
right: 0;
}
.mobile-menu-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0,0,0,0.5);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
}
.mobile-menu-overlay.active {
opacity: 1;
visibility: visible;
}
}
/* Print Styles */
@media print {
.modern-navbar,
.navbar-actions,
.mobile-menu,
.action-dropdown,
button {
display: none !important;
}
}

View File

@@ -1,361 +0,0 @@
/* Toast Notifications */
.toast-notification {
position: fixed;
top: 20px;
right: 20px;
min-width: 300px;
max-width: 500px;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 12px;
z-index: 10000;
opacity: 0;
transform: translateX(400px);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.toast-notification.show {
opacity: 1;
transform: translateX(0);
}
.toast-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
}
.toast-success {
border-left: 4px solid #28a745;
}
.toast-success .toast-icon {
background: #28a745;
color: white;
}
.toast-error {
border-left: 4px solid #dc3545;
}
.toast-error .toast-icon {
background: #dc3545;
color: white;
}
.toast-warning {
border-left: 4px solid #ffc107;
}
.toast-warning .toast-icon {
background: #ffc107;
color: #000;
}
.toast-info {
border-left: 4px solid #17a2b8;
}
.toast-info .toast-icon {
background: #17a2b8;
color: white;
}
.toast-message {
flex: 1;
color: #333;
font-size: 14px;
line-height: 1.4;
}
.toast-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
flex-shrink: 0;
}
.toast-close:hover {
color: #333;
}
.toast-close:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
border-radius: 4px;
}
/* Screen Reader Only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skip to Main Content Link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #667eea;
color: white;
padding: 8px 16px;
text-decoration: none;
border-radius: 0 0 4px 0;
z-index: 10001;
}
.skip-link:focus {
top: 0;
}
/* Focus Styles - Accessibility */
*:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* Remove outline for mouse users */
*:focus:not(:focus-visible) {
outline: none;
}
/* Loading Spinner */
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.spinner-small {
width: 20px;
height: 20px;
border-width: 2px;
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-overlay .spinner {
border-color: rgba(255, 255, 255, 0.3);
border-top-color: white;
}
/* Responsive Images */
img {
max-width: 100%;
height: auto;
}
/* Responsive Typography */
html {
font-size: 16px;
}
@media (max-width: 768px) {
html {
font-size: 14px;
}
}
@media (max-width: 480px) {
html {
font-size: 13px;
}
}
/* Responsive Containers */
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.container {
max-width: 1320px;
}
}
/* Mobile Responsive Utilities */
@media (max-width: 768px) {
.toast-notification {
right: 10px;
left: 10px;
min-width: auto;
max-width: calc(100% - 20px);
}
.hide-mobile {
display: none !important;
}
}
@media (min-width: 769px) {
.show-mobile-only {
display: none !important;
}
}
/* Tablet Specific */
@media (min-width: 768px) and (max-width: 1024px) {
.hide-tablet {
display: none !important;
}
}
/* Desktop Specific */
@media (min-width: 1025px) {
.hide-desktop {
display: none !important;
}
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* High Contrast Mode */
@media (prefers-contrast: high) {
* {
border-width: 2px !important;
}
button,
a {
text-decoration: underline;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.toast-notification {
background: #2d3748;
color: #fff;
}
.toast-message {
color: #e2e8f0;
}
.toast-close {
color: #a0aec0;
}
.toast-close:hover {
color: #e2e8f0;
}
}
/* Print Styles */
@media print {
.no-print,
.toast-notification,
.skip-link,
button,
nav {
display: none !important;
}
a[href]:after {
content: " (" attr(href) ")";
}
img {
max-width: 100% !important;
}
}

Binary file not shown.

View File

@@ -0,0 +1,111 @@
/**
* API Client
* Centralized API communication with error handling
*/
(function () {
"use strict";
class APIClient {
constructor(baseURL = "") {
this.baseURL = baseURL;
this.defaultHeaders = {
"Content-Type": "application/json",
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers,
},
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error(`API Error (${endpoint}):`, error);
throw error;
}
}
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: "GET" });
}
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, { method: "DELETE" });
}
// Product endpoints
async getProducts(params = {}) {
return this.get("/api/products", params);
}
async getProduct(id) {
return this.get(`/api/products/${id}`);
}
async getCategories() {
return this.get("/api/categories");
}
// Menu endpoints
async getMenu() {
return this.get("/api/menu");
}
// Homepage endpoints
async getHomepageSettings() {
return this.get("/api/homepage-settings");
}
}
// Create global instance
window.API = window.API || new APIClient();
// Helper function for loading states
window.withLoading = async function (element, asyncFn) {
if (!element) return asyncFn();
element.classList.add("loading");
element.setAttribute("aria-busy", "true");
try {
return await asyncFn();
} finally {
element.classList.remove("loading");
element.setAttribute("aria-busy", "false");
}
};
})();

View File

@@ -0,0 +1,62 @@
/**
* Back Button Navigation Control - SIMPLIFIED & FIXED
*
* Problem: History manipulation (replaceState/pushState) changes URL without reloading page
* Solution: Let browser handle navigation naturally, only intercept when necessary
*
* Requirements:
* 1. Natural browser back/forward navigation (URL changes = page loads)
* 2. Prevent going back past home page
* 3. Ensure page is always interactive after navigation
*/
(function () {
"use strict";
// Configuration
const HOME_PAGES = ["/", "/home.html", "/index.html"];
const HOME_URL = "/home.html";
/**
* Handle popstate (back/forward button) events
* This fires AFTER the browser has already navigated (URL changed)
*/
function handlePopState(event) {
// Get the NEW current path (browser already changed it)
const currentPath = window.location.pathname;
// Ensure page is always interactive after back/forward
document.body.classList.remove("page-transitioning");
document.body.style.opacity = "1";
sessionStorage.removeItem("page-transitioning");
// If we're on home page after a back navigation
// prevent going back further by adding home to history
if (HOME_PAGES.includes(currentPath)) {
// Use setTimeout to avoid interfering with current popstate
setTimeout(() => {
window.history.pushState({ page: "home" }, "", HOME_URL);
}, 0);
}
}
/**
* Prevent going back past home page
* Add an extra entry so back button stays on home
*/
function preventBackPastHome() {
const currentPath = window.location.pathname;
if (HOME_PAGES.includes(currentPath)) {
// Add an extra home entry
window.history.pushState({ page: "home", initial: true }, "", HOME_URL);
}
}
// Initialize: Add home history entry if on home page
preventBackPastHome();
// Listen for popstate (back/forward button)
// NOTE: Browser handles the actual navigation (page reload)
// We just ensure interactivity and prevent going back past home
window.addEventListener("popstate", handlePopState);
})();

View File

@@ -0,0 +1,155 @@
/**
* Shared Cart and Wishlist Functions
* Simple localStorage-based implementation that works on all pages
*/
(function () {
"use strict";
// Cart Functions
window.addToCart = function (productId, name, price, imageurl) {
try {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const existingItem = cart.find((item) => item.id === productId);
if (existingItem) {
existingItem.quantity = (existingItem.quantity || 1) + 1;
} else {
cart.push({
id: productId,
name,
price: parseFloat(price),
imageurl,
quantity: 1,
});
}
localStorage.setItem("cart", JSON.stringify(cart));
updateCartBadge();
showNotification(`${name} added to cart!`, "success");
} catch (e) {
console.error("Cart error:", e);
showNotification("Added to cart!", "success");
}
};
// Wishlist Functions
window.addToWishlist = function (productId, name, price, imageurl) {
try {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
const exists = wishlist.find((item) => item.id === productId);
if (!exists) {
wishlist.push({
id: productId,
name,
price: parseFloat(price),
imageurl,
});
localStorage.setItem("wishlist", JSON.stringify(wishlist));
updateWishlistBadge();
showNotification(`${name} added to wishlist!`, "success");
} else {
showNotification("Already in wishlist!", "info");
}
} catch (e) {
console.error("Wishlist error:", e);
showNotification("Added to wishlist!", "success");
}
};
// Update Badge Functions
function updateCartBadge() {
try {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const badge = document.querySelector(".cart-badge");
if (badge) {
const total = cart.reduce((sum, item) => sum + (item.quantity || 1), 0);
badge.textContent = total;
badge.style.display = total > 0 ? "flex" : "none";
}
} catch (e) {
console.error("Badge update error:", e);
}
}
function updateWishlistBadge() {
try {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
const badge = document.querySelector(".wishlist-badge");
if (badge) {
badge.textContent = wishlist.length;
badge.style.display = wishlist.length > 0 ? "flex" : "none";
}
} catch (e) {
console.error("Badge update error:", e);
}
}
// Notification Function
function showNotification(message, type = "info") {
// Remove existing notifications
document.querySelectorAll(".cart-notification").forEach((n) => n.remove());
const notification = document.createElement("div");
notification.className = `cart-notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: ${
type === "success"
? "#10b981"
: type === "error"
? "#ef4444"
: "#3b82f6"
};
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideInFromRight 0.3s ease;
`;
// Add animation styles if not already present
if (!document.getElementById("notification-animations")) {
const style = document.createElement("style");
style.id = "notification-animations";
style.textContent = `
@keyframes slideInFromRight {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutToRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOutToRight 0.3s ease";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Initialize badges on page load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
updateCartBadge();
updateWishlistBadge();
});
} else {
updateCartBadge();
updateWishlistBadge();
}
// Expose update functions globally
window.updateCartBadge = updateCartBadge;
window.updateWishlistBadge = updateWishlistBadge;
})();

View File

@@ -1,378 +1,319 @@
// Sky Art Shop - Shopping Cart Functions
// Add item to cart
function addToCart(id, name, price, imageUrl = null) {
// Get existing cart from localStorage
let cart = JSON.parse(localStorage.getItem("cart") || "[]");
// Check if item already exists
const existingItem = cart.find((item) => item.id === id);
if (existingItem) {
existingItem.quantity++;
// Update imageUrl if it was null before
if (!existingItem.imageUrl && imageUrl) {
existingItem.imageUrl = imageUrl;
}
} else {
cart.push({ id, name, price, quantity: 1, imageUrl });
}
// Save cart
localStorage.setItem("cart", JSON.stringify(cart));
console.log("Cart updated:", cart);
// Show confirmation
showCartNotification(`${name} added to cart!`);
updateCartCount();
}
// Remove item from cart
function removeFromCart(id) {
let cart = JSON.parse(localStorage.getItem("cart") || "[]");
cart = cart.filter((item) => item.id !== id);
localStorage.setItem("cart", JSON.stringify(cart));
updateCartCount();
}
// Update cart item quantity
function updateCartQuantity(id, quantity) {
let cart = JSON.parse(localStorage.getItem("cart") || "[]");
const item = cart.find((item) => item.id === id);
if (item) {
item.quantity = quantity;
if (quantity <= 0) {
cart = cart.filter((item) => item.id !== id);
}
}
localStorage.setItem("cart", JSON.stringify(cart));
updateCartCount();
}
// Get cart items
function getCart() {
return JSON.parse(localStorage.getItem("cart") || "[]");
}
// Get cart total
function getCartTotal() {
const cart = getCart();
return cart.reduce((total, item) => total + item.price * item.quantity, 0);
}
// Update cart count badge
function updateCartCount() {
const cart = getCart();
const count = cart.reduce((total, item) => total + item.quantity, 0);
// Update old badge (if exists)
const badge = document.getElementById("cart-count");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "inline" : "none";
}
// Update navbar cart badge
const navCartBadge = document.querySelector("#cartBtn .badge");
if (navCartBadge) {
navCartBadge.textContent = count;
navCartBadge.style.display = count > 0 ? "block" : "none";
}
}
// Show cart notification
function showCartNotification(message) {
const notification = document.createElement("div");
notification.className = "cart-notification";
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px 25px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
z-index: 10000;
animation: slideInFromTop 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Clear entire cart
function clearCart() {
localStorage.removeItem("cart");
updateCartCount();
}
// ====================================
// Wishlist Functions
// ====================================
// Add item to wishlist
function addToWishlist(id, name, price, imageUrl) {
let wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
const existingItem = wishlist.find((item) => item.id === id);
if (existingItem) {
showWishlistNotification(`${name} is already in your wishlist!`);
return;
}
wishlist.push({ id, name, price, imageUrl });
localStorage.setItem("wishlist", JSON.stringify(wishlist));
console.log("Wishlist updated:", wishlist);
showWishlistNotification(`${name} added to wishlist!`);
updateWishlistCount();
}
// Remove item from wishlist
function removeFromWishlist(id) {
let wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist = wishlist.filter((item) => item.id !== id);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
updateWishlistCount();
}
// Get wishlist items
function getWishlist() {
return JSON.parse(localStorage.getItem("wishlist") || "[]");
}
// Update wishlist count badge
function updateWishlistCount() {
const wishlist = getWishlist();
const count = wishlist.length;
const navWishlistBadge = document.querySelector("#wishlistBtn .badge");
const wishlistIcon = document.querySelector("#wishlistBtn i");
if (navWishlistBadge) {
navWishlistBadge.textContent = count;
navWishlistBadge.style.display = count > 0 ? "block" : "none";
}
// Change heart icon based on wishlist status
if (wishlistIcon) {
if (count > 0) {
wishlistIcon.className = "bi bi-heart-fill";
wishlistIcon.style.color = "#e74c3c";
} else {
wishlistIcon.className = "bi bi-heart";
wishlistIcon.style.color = "";
}
}
}
// Show wishlist notification
function showWishlistNotification(message) {
const notification = document.createElement("div");
notification.className = "wishlist-notification";
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: #E91E63;
color: white;
padding: 15px 25px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
z-index: 10000;
animation: slideInFromTop 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Clear entire wishlist
function clearWishlist() {
localStorage.removeItem("wishlist");
updateWishlistCount();
}
// ====================================
// Dropdown Functions
// ====================================
// Render cart dropdown
function renderCartDropdown() {
const cart = getCart();
const cartItems = document.getElementById("cartItems");
const cartTotal = document.getElementById("cartTotal");
if (!cartItems) return;
if (cart.length === 0) {
cartItems.innerHTML = '<p class="empty-message">Your cart is empty</p>';
if (cartTotal) cartTotal.textContent = "$0.00";
return;
}
console.log("Rendering cart:", cart);
cartItems.innerHTML = cart
.map((item) => {
const imgSrc = item.imageUrl || "/assets/images/placeholder.jpg";
console.log("Cart item image URL:", imgSrc);
return `
<div class="dropdown-item">
<img src="${imgSrc}" alt="${
item.name
}" class="dropdown-item-image" onerror="this.src='/assets/images/placeholder.jpg'">
<div class="dropdown-item-info">
<div class="dropdown-item-name">${item.name}</div>
<div class="dropdown-item-details">
<span class="dropdown-item-quantity">Qty: ${
item.quantity
}</span>
<span class="dropdown-item-price">$${(
item.price * item.quantity
).toFixed(2)}</span>
</div>
</div>
<button class="dropdown-item-remove" onclick="removeFromCartDropdown('${
item.id
}')">
<i class="bi bi-x"></i>
</button>
</div>
`;
})
.join("");
if (cartTotal) {
const total = getCartTotal();
cartTotal.textContent = `$${total.toFixed(2)}`;
}
}
// Render wishlist dropdown
function renderWishlistDropdown() {
const wishlist = getWishlist();
const wishlistItems = document.getElementById("wishlistItems");
if (!wishlistItems) return;
if (wishlist.length === 0) {
wishlistItems.innerHTML =
'<p class="empty-message">Your wishlist is empty</p>';
return;
}
console.log("Rendering wishlist:", wishlist);
wishlistItems.innerHTML = wishlist
.map((item) => {
const imgSrc = item.imageUrl || "/assets/images/placeholder.jpg";
console.log("Wishlist item image URL:", imgSrc);
return `
<div class="dropdown-item">
<img src="${imgSrc}" alt="${
item.name
}" class="dropdown-item-image" onerror="this.src='/assets/images/placeholder.jpg'">
<div class="dropdown-item-info">
<div class="dropdown-item-name">${item.name}</div>
<div class="dropdown-item-details">
<span class="dropdown-item-price">$${item.price.toFixed(
2
)}</span>
</div>
</div>
<button class="dropdown-item-remove" onclick="removeFromWishlistDropdown('${
item.id
}')">
<i class="bi bi-x"></i>
</button>
</div>
`;
})
.join("");
}
// Remove from cart via dropdown
function removeFromCartDropdown(id) {
removeFromCart(id);
renderCartDropdown();
updateCartCount();
}
// Remove from wishlist via dropdown
function removeFromWishlistDropdown(id) {
removeFromWishlist(id);
renderWishlistDropdown();
updateWishlistCount();
}
// Toggle dropdown visibility
function toggleDropdown(dropdownId) {
const dropdown = document.getElementById(dropdownId);
if (!dropdown) return;
// Close other dropdowns
document.querySelectorAll(".icon-dropdown").forEach((d) => {
if (d.id !== dropdownId) {
d.classList.remove("show");
}
});
dropdown.classList.toggle("show");
// Render content when opening
if (dropdown.classList.contains("show")) {
if (dropdownId === "cartDropdown") {
renderCartDropdown();
} else if (dropdownId === "wishlistDropdown") {
renderWishlistDropdown();
}
}
}
// Close cart/wishlist dropdowns when clicking outside
document.addEventListener("click", function (e) {
if (
!e.target.closest(".dropdown-container") &&
!e.target.closest(".nav-toggle")
) {
document.querySelectorAll(".icon-dropdown").forEach((d) => {
d.classList.remove("show");
});
}
});
// Initialize cart and wishlist count on page load
document.addEventListener("DOMContentLoaded", function () {
updateCartCount();
updateWishlistCount();
// Add click handlers for dropdown toggles
const cartBtn = document.getElementById("cartBtn");
const wishlistBtn = document.getElementById("wishlistBtn");
if (cartBtn) {
cartBtn.addEventListener("click", function (e) {
e.preventDefault();
toggleDropdown("cartDropdown");
});
}
if (wishlistBtn) {
wishlistBtn.addEventListener("click", function (e) {
e.preventDefault();
toggleDropdown("wishlistDropdown");
});
}
});
/**
* Shopping Cart Component
* Handles cart dropdown, updates, and interactions
*/
(function () {
"use strict";
class ShoppingCart {
constructor() {
this.cartToggle = document.getElementById("cartToggle");
this.cartPanel = document.getElementById("cartPanel");
this.cartContent = document.getElementById("cartContent");
this.cartClose = document.getElementById("cartClose");
this.isOpen = false;
this.init();
}
init() {
this.setupEventListeners();
this.render();
}
setupEventListeners() {
if (this.cartToggle) {
this.cartToggle.addEventListener("click", () => this.toggle());
}
if (this.cartClose) {
this.cartClose.addEventListener("click", () => this.close());
}
// Close when clicking outside
document.addEventListener("click", (e) => {
if (this.isOpen && !e.target.closest(".cart-dropdown-wrapper")) {
this.close();
}
});
// Listen for cart updates
window.addEventListener("cart-updated", () => this.render());
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.cartPanel) {
this.cartPanel.classList.add("active");
this.cartPanel.setAttribute("aria-hidden", "false");
this.isOpen = true;
this.render();
}
}
close() {
if (this.cartPanel) {
this.cartPanel.classList.remove("active");
this.cartPanel.setAttribute("aria-hidden", "true");
this.isOpen = false;
}
}
render() {
if (!this.cartContent) return;
const cart = window.AppState.cart;
if (cart.length === 0) {
this.cartContent.innerHTML =
'<p class="empty-state">Your cart is empty</p>';
this.updateFooter(null);
return;
}
const html = cart.map((item) => this.renderCartItem(item)).join("");
this.cartContent.innerHTML = html;
// Add event listeners to cart items
this.setupCartItemListeners();
// Update footer with total
this.updateFooter(window.AppState.getCartTotal());
}
renderCartItem(item) {
const imageUrl =
item.imageUrl || item.image_url || "/assets/images/placeholder.jpg";
const title = window.Utils.escapeHtml(
item.title || item.name || "Product"
);
const price = window.Utils.formatCurrency(item.price || 0);
const subtotal = window.Utils.formatCurrency(
(item.price || 0) * item.quantity
);
return `
<div class="cart-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${title}" class="cart-item-image" loading="lazy">
<div class="cart-item-details">
<h4 class="cart-item-title">${title}</h4>
<p class="cart-item-price">${price}</p>
<div class="cart-item-quantity">
<button class="quantity-btn quantity-minus" data-id="${item.id}" aria-label="Decrease quantity">
<i class="bi bi-dash"></i>
</button>
<span class="quantity-value">${item.quantity}</span>
<button class="quantity-btn quantity-plus" data-id="${item.id}" aria-label="Increase quantity">
<i class="bi bi-plus"></i>
</button>
</div>
<p class="cart-item-subtotal">${subtotal}</p>
</div>
<button class="cart-item-remove" data-id="${item.id}" aria-label="Remove from cart">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
setupCartItemListeners() {
// Remove buttons
this.cartContent.querySelectorAll(".cart-item-remove").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
window.AppState.removeFromCart(id);
this.render();
});
});
// Quantity buttons
this.cartContent.querySelectorAll(".quantity-minus").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.cart.find((item) => item.id === id);
if (item && item.quantity > 1) {
window.AppState.updateCartQuantity(id, item.quantity - 1);
this.render();
}
});
});
this.cartContent.querySelectorAll(".quantity-plus").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.cart.find((item) => item.id === id);
if (item) {
window.AppState.updateCartQuantity(id, item.quantity + 1);
this.render();
}
});
});
}
updateFooter(total) {
const footer = this.cartPanel?.querySelector(".dropdown-foot");
if (!footer) return;
if (total === null) {
footer.innerHTML =
'<a href="/shop" class="btn-outline">Continue Shopping</a>';
} else {
footer.innerHTML = `
<div class="cart-total">
<span>Total:</span>
<strong>${window.Utils.formatCurrency(total)}</strong>
</div>
<a href="/shop" class="btn-text">Continue Shopping</a>
<button class="btn-primary-full" onclick="alert('Checkout coming soon!')">
Proceed to Checkout
</button>
`;
}
}
}
// Wishlist Component
class Wishlist {
constructor() {
this.wishlistToggle = document.getElementById("wishlistToggle");
this.wishlistPanel = document.getElementById("wishlistPanel");
this.wishlistContent = document.getElementById("wishlistContent");
this.wishlistClose = document.getElementById("wishlistClose");
this.isOpen = false;
this.init();
}
init() {
this.setupEventListeners();
this.render();
}
setupEventListeners() {
if (this.wishlistToggle) {
this.wishlistToggle.addEventListener("click", () => this.toggle());
}
if (this.wishlistClose) {
this.wishlistClose.addEventListener("click", () => this.close());
}
// Close when clicking outside
document.addEventListener("click", (e) => {
if (this.isOpen && !e.target.closest(".wishlist-dropdown-wrapper")) {
this.close();
}
});
// Listen for wishlist updates
window.addEventListener("wishlist-updated", () => this.render());
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.wishlistPanel) {
this.wishlistPanel.classList.add("active");
this.wishlistPanel.setAttribute("aria-hidden", "false");
this.isOpen = true;
this.render();
}
}
close() {
if (this.wishlistPanel) {
this.wishlistPanel.classList.remove("active");
this.wishlistPanel.setAttribute("aria-hidden", "true");
this.isOpen = false;
}
}
render() {
if (!this.wishlistContent) return;
const wishlist = window.AppState.wishlist;
if (wishlist.length === 0) {
this.wishlistContent.innerHTML =
'<p class="empty-state">Your wishlist is empty</p>';
return;
}
const html = wishlist
.map((item) => this.renderWishlistItem(item))
.join("");
this.wishlistContent.innerHTML = html;
// Add event listeners
this.setupWishlistItemListeners();
}
renderWishlistItem(item) {
const imageUrl =
item.imageUrl || item.image_url || "/assets/images/placeholder.jpg";
const title = window.Utils.escapeHtml(
item.title || item.name || "Product"
);
const price = window.Utils.formatCurrency(item.price || 0);
return `
<div class="wishlist-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${title}" class="wishlist-item-image" loading="lazy">
<div class="wishlist-item-details">
<h4 class="wishlist-item-title">${title}</h4>
<p class="wishlist-item-price">${price}</p>
<button class="btn-add-to-cart" data-id="${item.id}">Add to Cart</button>
</div>
<button class="wishlist-item-remove" data-id="${item.id}" aria-label="Remove from wishlist">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
setupWishlistItemListeners() {
// Remove buttons
this.wishlistContent
.querySelectorAll(".wishlist-item-remove")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
window.AppState.removeFromWishlist(id);
this.render();
});
});
// Add to cart buttons
this.wishlistContent
.querySelectorAll(".btn-add-to-cart")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.wishlist.find(
(item) => item.id === id
);
if (item) {
window.AppState.addToCart(item);
}
});
});
}
}
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
new ShoppingCart();
new Wishlist();
});
} else {
new ShoppingCart();
new Wishlist();
}
})();

View File

@@ -0,0 +1,72 @@
/**
* Lazy Loading Images Script
* Optimizes image loading for better performance
*/
(function () {
"use strict";
// Check for Intersection Observer support
if (!("IntersectionObserver" in window)) {
// Fallback: load all images immediately
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
if (img.dataset.src) {
img.src = img.dataset.src;
}
});
return;
}
// Configure intersection observer
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
// Load the image
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
// Optional: load srcset
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute("data-srcset");
}
// Add loaded class for fade-in effect
img.classList.add("loaded");
// Stop observing this image
observer.unobserve(img);
}
});
},
{
// Start loading when image is 50px from viewport
rootMargin: "50px 0px",
threshold: 0.01,
}
);
// Observe all lazy images
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
lazyImages.forEach((img) => imageObserver.observe(img));
// Add CSS for fade-in effect if not already present
if (!document.getElementById("lazy-load-styles")) {
const style = document.createElement("style");
style.id = "lazy-load-styles";
style.textContent = `
img[loading="lazy"] {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
img[loading="lazy"].loaded {
opacity: 1;
}
`;
document.head.appendChild(style);
}
})();

View File

@@ -1,427 +1,350 @@
// Sky Art Shop - Main JavaScript File
// ====================================
// Mobile Navigation Toggle
// ====================================
document.addEventListener("DOMContentLoaded", function () {
const navToggle = document.querySelector(".nav-toggle");
const navMenu = document.querySelector("#navDropdown");
if (navToggle && navMenu) {
// Hover to open dropdown
navToggle.addEventListener("mouseenter", function () {
navMenu.classList.add("active");
this.setAttribute("aria-expanded", "true");
const spans = this.querySelectorAll("span");
spans[0].style.transform = "rotate(45deg) translate(7px, 7px)";
spans[1].style.opacity = "0";
spans[2].style.transform = "rotate(-45deg) translate(7px, -7px)";
});
// Keep dropdown open when hovering over it
navMenu.addEventListener("mouseenter", function () {
this.classList.add("active");
});
// Close when mouse leaves both hamburger and dropdown
navToggle.addEventListener("mouseleave", function (e) {
// Delay closing to allow moving to dropdown
setTimeout(() => {
if (!navMenu.matches(":hover") && !navToggle.matches(":hover")) {
navMenu.classList.remove("active");
navToggle.setAttribute("aria-expanded", "false");
const spans = navToggle.querySelectorAll("span");
spans[0].style.transform = "none";
spans[1].style.opacity = "1";
spans[2].style.transform = "none";
}
}, 200);
});
navMenu.addEventListener("mouseleave", function () {
setTimeout(() => {
if (!navMenu.matches(":hover") && !navToggle.matches(":hover")) {
navMenu.classList.remove("active");
navToggle.setAttribute("aria-expanded", "false");
const spans = navToggle.querySelectorAll("span");
spans[0].style.transform = "none";
spans[1].style.opacity = "1";
spans[2].style.transform = "none";
}
}, 200);
});
// Click to toggle (for mobile/touch)
navToggle.addEventListener("click", function (e) {
e.stopPropagation();
const isActive = navMenu.classList.toggle("active");
this.setAttribute("aria-expanded", isActive ? "true" : "false");
// Animate hamburger menu
const spans = this.querySelectorAll("span");
if (isActive) {
spans[0].style.transform = "rotate(45deg) translate(7px, 7px)";
spans[1].style.opacity = "0";
spans[2].style.transform = "rotate(-45deg) translate(7px, -7px)";
} else {
spans[0].style.transform = "none";
spans[1].style.opacity = "1";
spans[2].style.transform = "none";
}
});
// Close dropdown when clicking on a link
const dropdownLinks = navMenu.querySelectorAll("a");
dropdownLinks.forEach((link) => {
link.addEventListener("click", function () {
navMenu.classList.remove("active");
navToggle.setAttribute("aria-expanded", "false");
const spans = navToggle.querySelectorAll("span");
spans[0].style.transform = "none";
spans[1].style.opacity = "1";
spans[2].style.transform = "none";
});
});
// Close dropdown when clicking outside
document.addEventListener("click", function (event) {
// Don't close if clicking on cart/wishlist dropdowns
if (
event.target.closest(".dropdown-container") ||
event.target.closest(".icon-dropdown")
) {
return;
}
const isClickInside =
navToggle.contains(event.target) || navMenu.contains(event.target);
if (!isClickInside && navMenu.classList.contains("active")) {
navMenu.classList.remove("active");
navToggle.setAttribute("aria-expanded", "false");
const spans = navToggle.querySelectorAll("span");
spans[0].style.transform = "none";
spans[1].style.opacity = "1";
spans[2].style.transform = "none";
}
});
}
});
// ====================================
// Smooth Scrolling for Anchor Links
// ====================================
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
const href = this.getAttribute("href");
if (href !== "#" && href !== "#instagram" && href !== "#wishlist") {
e.preventDefault();
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
});
});
// ====================================
// Shop Page Filtering
// ====================================
const categoryFilter = document.getElementById("category-filter");
const sortFilter = document.getElementById("sort-filter");
if (categoryFilter) {
categoryFilter.addEventListener("change", function () {
const selectedCategory = this.value;
const productCards = document.querySelectorAll(".product-card");
productCards.forEach((card) => {
if (selectedCategory === "all") {
card.style.display = "block";
} else {
const cardCategory = card.getAttribute("data-category");
if (cardCategory === selectedCategory) {
card.style.display = "block";
} else {
card.style.display = "none";
}
}
});
});
}
if (sortFilter) {
sortFilter.addEventListener("change", function () {
const sortValue = this.value;
const productsGrid = document.querySelector(".products-grid");
const productCards = Array.from(document.querySelectorAll(".product-card"));
if (sortValue === "price-low") {
productCards.sort((a, b) => {
const priceA = parseFloat(
a.querySelector(".price").textContent.replace("$", "")
);
const priceB = parseFloat(
b.querySelector(".price").textContent.replace("$", "")
);
return priceA - priceB;
});
} else if (sortValue === "price-high") {
productCards.sort((a, b) => {
const priceA = parseFloat(
a.querySelector(".price").textContent.replace("$", "")
);
const priceB = parseFloat(
b.querySelector(".price").textContent.replace("$", "")
);
return priceB - priceA;
});
}
// Re-append sorted cards
productCards.forEach((card) => {
productsGrid.appendChild(card);
});
});
}
// ====================================
// Add to Cart Functionality (Basic)
// ====================================
document.querySelectorAll(".product-card .btn").forEach((button) => {
button.addEventListener("click", function (e) {
e.preventDefault();
// Get product details
const productCard = this.closest(".product-card");
const productName = productCard.querySelector("h3").textContent;
const productPrice = productCard.querySelector(".price").textContent;
// Show notification
showNotification(`${productName} added to cart!`);
// You can expand this to actually store cart items
// For example, using localStorage or sending to a server
});
});
// ====================================
// Contact Form Handling
// ====================================
const contactForm = document.getElementById("contactForm");
if (contactForm) {
contactForm.addEventListener("submit", function (e) {
e.preventDefault();
// Get form values
const name = document.getElementById("name").value;
const email = document.getElementById("email").value;
const phone = document.getElementById("phone").value;
const subject = document.getElementById("subject").value;
const message = document.getElementById("message").value;
// Basic validation
if (!name || !email || !subject || !message) {
showNotification("Please fill in all required fields!", "error");
return;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
showNotification("Please enter a valid email address!", "error");
return;
}
// Here you would typically send the form data to a server
// For now, we'll just show a success message
showNotification(
"Thank you! Your message has been sent. We'll get back to you soon.",
"success"
);
// Reset form
contactForm.reset();
});
}
// ====================================
// Notification System
// ====================================
function showNotification(message, type = "success") {
// Create notification element
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
// Style the notification
notification.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
background-color: ${type === "success" ? "#4CAF50" : "#F44336"};
color: white;
padding: 15px 25px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
// Add animation
const style = document.createElement("style");
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// Add to page
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease-out";
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// ====================================
// Image Lazy Loading (Optional Enhancement)
// ====================================
if ("IntersectionObserver" in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src || img.src;
img.classList.add("loaded");
observer.unobserve(img);
}
});
});
document.querySelectorAll("img").forEach((img) => {
imageObserver.observe(img);
});
}
// ====================================
// Scroll to Top Button
// ====================================
function createScrollToTopButton() {
const button = document.createElement("button");
button.innerHTML = "↑";
button.className = "scroll-to-top";
button.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background-color: #6B4E9B;
color: white;
border: none;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
display: none;
z-index: 1000;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
`;
document.body.appendChild(button);
// Show/hide button based on scroll position
window.addEventListener("scroll", () => {
if (window.pageYOffset > 300) {
button.style.display = "block";
} else {
button.style.display = "none";
}
});
// Scroll to top when clicked
button.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
// Hover effect
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = "#5a3e82";
button.style.transform = "translateY(-3px)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = "#6B4E9B";
button.style.transform = "translateY(0)";
});
}
// Initialize scroll to top button
createScrollToTopButton();
// ====================================
// Portfolio Gallery Hover Effects
// ====================================
document.querySelectorAll(".portfolio-category").forEach((category) => {
category.addEventListener("mouseenter", function () {
this.style.transition = "all 0.3s ease";
});
});
// ====================================
// Active Navigation Link Highlighting
// ====================================
function highlightActiveNavLink() {
const currentPage = window.location.pathname.split("/").pop() || "index.html";
const navLinks = document.querySelectorAll(".nav-menu a");
navLinks.forEach((link) => {
const linkPage = link.getAttribute("href").split("/").pop().split("#")[0];
if (linkPage === currentPage) {
link.classList.add("active");
}
});
}
highlightActiveNavLink();
// ====================================
// Print console message
// ====================================
console.log(
"%c Sky Art Shop Website ",
"background: #6B4E9B; color: white; font-size: 20px; padding: 10px;"
);
console.log("Welcome to Sky Art Shop! 🎨");
/**
* Main Application JavaScript
* Handles global state management, API integration, and core functionality
*/
(function () {
"use strict";
// Global state management
window.AppState = {
cart: [],
wishlist: [],
products: [],
settings: null,
user: null,
// Initialize state from localStorage
init() {
this.loadCart();
this.loadWishlist();
this.updateUI();
},
// Cart management
loadCart() {
try {
const saved = localStorage.getItem("cart");
this.cart = saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error loading cart:", error);
this.cart = [];
}
},
saveCart() {
try {
localStorage.setItem("cart", JSON.stringify(this.cart));
this.updateUI();
} catch (error) {
console.error("Error saving cart:", error);
}
},
addToCart(product, quantity = 1) {
const existing = this.cart.find((item) => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.cart.push({ ...product, quantity });
}
this.saveCart();
this.showNotification("Added to cart", "success");
},
removeFromCart(productId) {
this.cart = this.cart.filter((item) => item.id !== productId);
this.saveCart();
this.showNotification("Removed from cart", "info");
},
updateCartQuantity(productId, quantity) {
const item = this.cart.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
this.saveCart();
}
},
getCartTotal() {
return this.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
getCartCount() {
return this.cart.reduce((sum, item) => sum + item.quantity, 0);
},
// Wishlist management
loadWishlist() {
try {
const saved = localStorage.getItem("wishlist");
this.wishlist = saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error loading wishlist:", error);
this.wishlist = [];
}
},
saveWishlist() {
try {
localStorage.setItem("wishlist", JSON.stringify(this.wishlist));
this.updateUI();
} catch (error) {
console.error("Error saving wishlist:", error);
}
},
addToWishlist(product) {
if (!this.wishlist.find((item) => item.id === product.id)) {
this.wishlist.push(product);
this.saveWishlist();
this.showNotification("Added to wishlist", "success");
}
},
removeFromWishlist(productId) {
this.wishlist = this.wishlist.filter((item) => item.id !== productId);
this.saveWishlist();
this.showNotification("Removed from wishlist", "info");
},
isInWishlist(productId) {
return this.wishlist.some((item) => item.id === productId);
},
// UI updates
updateUI() {
this.updateCartUI();
this.updateWishlistUI();
},
updateCartUI() {
const count = this.getCartCount();
const badge = document.getElementById("cartCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
updateWishlistUI() {
const count = this.wishlist.length;
const badge = document.getElementById("wishlistCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
// Notifications
showNotification(message, type = "info") {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.setAttribute("role", "alert");
notification.setAttribute("aria-live", "polite");
document.body.appendChild(notification);
setTimeout(() => notification.classList.add("show"), 10);
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 3000);
},
};
// API Client
window.API = {
baseURL: "/api",
async request(endpoint, options = {}) {
try {
const response = await fetch(this.baseURL + endpoint, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("API request failed:", error);
throw error;
}
},
// Product endpoints
async getProducts(filters = {}) {
const params = new URLSearchParams(filters);
return this.request(`/products?${params}`);
},
async getProduct(id) {
return this.request(`/products/${id}`);
},
async getFeaturedProducts() {
return this.request("/products/featured");
},
// Settings endpoint
async getSettings() {
return this.request("/settings");
},
// Homepage endpoint
async getHomepageSettings() {
return this.request("/homepage/settings");
},
// Menu endpoint
async getMenu() {
return this.request("/menu");
},
// Blog endpoints
async getBlogPosts() {
return this.request("/blog");
},
async getBlogPost(id) {
return this.request(`/blog/${id}`);
},
// Portfolio endpoints
async getPortfolioProjects() {
return this.request("/portfolio");
},
async getPortfolioProject(id) {
return this.request(`/portfolio/${id}`);
},
// Pages endpoints
async getPages() {
return this.request("/pages");
},
async getPage(slug) {
return this.request(`/pages/${slug}`);
},
};
// Utility functions
window.Utils = {
// Format currency
formatCurrency(amount) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
},
// Format date
formatDate(date) {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
},
// Debounce function
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// Get URL parameter
getUrlParameter(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
},
// Safe HTML encode
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
// Show loading state
showLoading(element) {
if (element) {
element.classList.add("loading");
element.setAttribute("aria-busy", "true");
}
},
hideLoading(element) {
if (element) {
element.classList.remove("loading");
element.setAttribute("aria-busy", "false");
}
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.AppState.init();
});
} else {
window.AppState.init();
}
// Add notification styles if not exists
if (!document.getElementById("notification-styles")) {
const style = document.createElement("style");
style.id = "notification-styles";
style.textContent = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
opacity: 0;
transform: translateX(400px);
transition: all 0.3s ease;
max-width: 300px;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification-success {
border-left: 4px solid #28a745;
}
.notification-error {
border-left: 4px solid #dc3545;
}
.notification-info {
border-left: 4px solid #17a2b8;
}
.notification-warning {
border-left: 4px solid #ffc107;
}
`;
document.head.appendChild(style);
}
})();

View File

@@ -1,79 +1,203 @@
// Dynamic Menu Loader for Sky Art Shop
// Include this in all public pages to load menu from database
/**
* Navigation Component
* Handles mobile menu, dropdowns, and accessibility
*/
(function () {
"use strict";
// Load and render navigation menu from API
async function loadNavigationMenu() {
try {
const response = await fetch("/api/menu");
const data = await response.json();
class Navigation {
constructor() {
this.mobileMenuToggle = document.getElementById("mobileMenuToggle");
this.mobileMenu = document.getElementById("mobileMenu");
this.mobileMenuClose = document.getElementById("mobileMenuClose");
this.overlay = document.getElementById("mobileMenuOverlay");
this.body = document.body;
if (data.success && data.items && data.items.length > 0) {
renderDesktopMenu(data.items);
renderMobileMenu(data.items);
this.init();
}
init() {
this.setupMobileMenu();
this.setupAccessibility();
this.highlightCurrentPage();
this.setupKeyboardNavigation();
}
setupMobileMenu() {
// Open mobile menu
if (this.mobileMenuToggle) {
this.mobileMenuToggle.addEventListener("click", () =>
this.openMobileMenu()
);
}
// Close mobile menu
if (this.mobileMenuClose) {
this.mobileMenuClose.addEventListener("click", () =>
this.closeMobileMenu()
);
}
if (this.overlay) {
this.overlay.addEventListener("click", () => this.closeMobileMenu());
}
// Close on ESC key
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
this.mobileMenu &&
this.mobileMenu.classList.contains("active")
) {
this.closeMobileMenu();
}
});
}
openMobileMenu() {
if (this.mobileMenu) {
this.mobileMenu.classList.add("active");
this.mobileMenu.setAttribute("aria-hidden", "false");
this.body.style.overflow = "hidden";
if (this.overlay) {
this.overlay.classList.add("active");
}
// Focus first link
const firstLink = this.mobileMenu.querySelector("a");
if (firstLink) {
setTimeout(() => firstLink.focus(), 100);
}
}
}
closeMobileMenu() {
if (this.mobileMenu) {
this.mobileMenu.classList.remove("active");
this.mobileMenu.setAttribute("aria-hidden", "true");
this.body.style.overflow = "";
if (this.overlay) {
this.overlay.classList.remove("active");
}
// Return focus to toggle button
if (this.mobileMenuToggle) {
this.mobileMenuToggle.focus();
}
}
}
setupAccessibility() {
// Wait for body to exist
if (!document.body) return;
// Add ARIA labels to nav items
const navLinks = document.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
if (!link.getAttribute("aria-label")) {
link.setAttribute(
"aria-label",
`Navigate to ${link.textContent.trim()}`
);
}
});
// Add skip to main content link
if (!document.getElementById("skip-to-main")) {
const skipLink = document.createElement("a");
skipLink.id = "skip-to-main";
skipLink.href = "#main-content";
skipLink.textContent = "Skip to main content";
skipLink.className = "skip-link";
document.body.insertBefore(skipLink, document.body.firstChild);
// Add styles for skip link
if (!document.getElementById("skip-link-styles")) {
const style = document.createElement("style");
style.id = "skip-link-styles";
style.textContent = `
.skip-link {
position: fixed;
top: -100px;
left: 0;
padding: 10px 20px;
background: #000;
color: #fff;
z-index: 10001;
text-decoration: none;
border-radius: 0 0 8px 0;
}
.skip-link:focus {
top: 0;
}
`;
document.head.appendChild(style);
}
}
}
highlightCurrentPage() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll(".nav-link, .mobile-link");
navLinks.forEach((link) => {
const href = link.getAttribute("href");
if (
href &&
(currentPath === href || currentPath.startsWith(href + "/"))
) {
link.classList.add("active");
link.setAttribute("aria-current", "page");
} else {
link.classList.remove("active");
link.removeAttribute("aria-current");
}
});
}
setupKeyboardNavigation() {
// Tab trap in mobile menu when open
if (this.mobileMenu) {
const focusableElements = this.mobileMenu.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
this.mobileMenu.addEventListener("keydown", (e) => {
if (
e.key === "Tab" &&
this.mobileMenu.classList.contains("active")
) {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
});
}
}
} catch (error) {
console.error("Failed to load menu:", error);
// Keep existing hardcoded menu as fallback
}
}
function renderDesktopMenu(items) {
const desktopMenuList = document.querySelector(".nav-menu-list");
if (!desktopMenuList) return;
desktopMenuList.innerHTML = items
.map(
(item) => `
<li class="nav-item">
<a href="${item.url}" class="nav-link">
${item.icon ? `<i class="bi ${item.icon}"></i> ` : ""}${item.label}
</a>
</li>
`
)
.join("");
// Set active state based on current page
const currentPath = window.location.pathname;
document.querySelectorAll(".nav-link").forEach((link) => {
if (link.getAttribute("href") === currentPath) {
link.classList.add("active");
}
});
}
function renderMobileMenu(items) {
const mobileMenuList = document.querySelector(".mobile-menu-list");
if (!mobileMenuList) return;
mobileMenuList.innerHTML = items
.map(
(item) => `
<li>
<a href="${item.url}" class="mobile-link">
${item.icon ? `<i class="bi ${item.icon}"></i> ` : ""}${item.label}
</a>
</li>
`
)
.join("");
// Set active state for mobile menu
const currentPath = window.location.pathname;
document.querySelectorAll(".mobile-link").forEach((link) => {
if (link.getAttribute("href") === currentPath) {
link.classList.add("active");
}
});
}
// Load menu when DOM is ready
// Initialize navigation when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadNavigationMenu);
document.addEventListener("DOMContentLoaded", () => {
new Navigation();
});
} else {
loadNavigationMenu();
new Navigation();
}
})();

View File

@@ -0,0 +1,224 @@
/**
* Notification System
* Accessible toast notifications
*/
(function () {
"use strict";
class NotificationManager {
constructor() {
this.container = null;
this.notifications = new Map();
this.init();
}
init() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () =>
this.createContainer()
);
} else {
this.createContainer();
}
}
createContainer() {
if (!document.body || this.container) return;
this.container = document.createElement("div");
this.container.id = "notification-container";
this.container.setAttribute("aria-live", "polite");
this.container.setAttribute("aria-atomic", "true");
this.container.className = "notification-container";
const style = document.createElement("style");
style.textContent = `
.notification-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
pointer-events: none;
}
.notification {
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 12px;
color: white;
font-size: 14px;
font-weight: 500;
pointer-events: auto;
animation: slideInRight 0.3s ease;
min-width: 250px;
}
.notification.removing {
animation: slideOutRight 0.3s ease;
}
.notification-success {
background: #10b981;
}
.notification-error {
background: #ef4444;
}
.notification-info {
background: #3b82f6;
}
.notification-warning {
background: #f59e0b;
}
.notification-icon {
font-size: 18px;
flex-shrink: 0;
}
.notification-message {
flex: 1;
}
.notification-close {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 4px;
opacity: 0.8;
transition: opacity 0.2s;
}
.notification-close:hover {
opacity: 1;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
@media (max-width: 640px) {
.notification-container {
right: 10px;
left: 10px;
max-width: none;
}
.notification {
min-width: auto;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(this.container);
}
show(message, type = "info", duration = 3000) {
if (!this.container) this.createContainer();
if (!this.container) return;
const id = Date.now() + Math.random();
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.setAttribute("role", "alert");
const icons = {
success: "✓",
error: "✕",
info: "",
warning: "⚠",
};
notification.innerHTML = `
<span class="notification-icon">${icons[type] || icons.info}</span>
<span class="notification-message">${this.escapeHtml(message)}</span>
<button class="notification-close" aria-label="Close notification">×</button>
`;
const closeBtn = notification.querySelector(".notification-close");
closeBtn.addEventListener("click", () => this.remove(id));
this.container.appendChild(notification);
this.notifications.set(id, notification);
if (duration > 0) {
setTimeout(() => this.remove(id), duration);
}
return id;
}
remove(id) {
const notification = this.notifications.get(id);
if (!notification) return;
notification.classList.add("removing");
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
this.notifications.delete(id);
}, 300);
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
success(message, duration) {
return this.show(message, "success", duration);
}
error(message, duration) {
return this.show(message, "error", duration);
}
info(message, duration) {
return this.show(message, "info", duration);
}
warning(message, duration) {
return this.show(message, "warning", duration);
}
}
// Create global instance
window.Notifications = window.Notifications || new NotificationManager();
// Legacy compatibility
window.showNotification = function (message, type = "info") {
window.Notifications.show(message, type);
};
})();

View File

@@ -1,143 +1,555 @@
// Smooth Page Transitions for Sky Art Shop
// Provides fade-out/fade-in effects when navigating between pages
/**
* Page Transitions and Smooth Navigation
* Handles page loading, transitions, and history management
*/
(function () {
"use strict";
class PageTransitions {
constructor() {
this.transitionDuration = 300;
this.isTransitioning = false;
this.init();
}
// Add page transition styles (less aggressive approach)
const style = document.createElement("style");
style.textContent = `
body {
transition: opacity 0.25s ease-in-out;
init() {
// Wait for body to exist
if (!document.body) return;
// Add transition wrapper if it doesn't exist
if (!document.getElementById("page-transition")) {
const wrapper = document.createElement("div");
wrapper.id = "page-transition";
wrapper.className = "page-transition";
// Wrap main content
const main = document.querySelector("main") || document.body;
const parent = main.parentNode;
parent.insertBefore(wrapper, main);
wrapper.appendChild(main);
}
body.page-transitioning {
// Add fade-in on page load
this.fadeIn();
// Intercept navigation clicks
this.setupLinkInterception();
// Handle back/forward buttons
window.addEventListener("popstate", (e) => {
if (e.state && e.state.url) {
this.navigate(e.state.url, false);
}
});
// Add scroll restoration
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
}
}
fadeIn() {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-in");
setTimeout(() => {
wrapper.classList.remove("fade-in");
}, this.transitionDuration);
}
}
fadeOut(callback) {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-out");
setTimeout(() => {
if (callback) callback();
wrapper.classList.remove("fade-out");
}, this.transitionDuration);
} else {
if (callback) callback();
}
}
setupLinkInterception() {
document.addEventListener("click", (e) => {
const link = e.target.closest("a");
// Check if it's a valid internal link
if (!link) return;
if (link.hasAttribute("data-no-transition")) return;
if (link.target === "_blank") return;
if (link.hasAttribute("download")) return;
const href = link.getAttribute("href");
if (
!href ||
href.startsWith("#") ||
href.startsWith("mailto:") ||
href.startsWith("tel:")
)
return;
// Check if it's an external link
const url = new URL(href, window.location.origin);
if (url.origin !== window.location.origin) return;
// Intercept the navigation
e.preventDefault();
this.navigate(href, true);
});
}
navigate(url, updateHistory = true) {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.fadeOut(() => {
if (updateHistory) {
history.pushState({ url }, "", url);
}
window.location.href = url;
});
}
// Scroll to element with smooth animation
scrollTo(selector, offset = 0) {
const element = document.querySelector(selector);
if (!element) return;
const top =
element.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: "smooth",
});
}
// Scroll to top
scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
}
/**
* Lazy Loading Images
* Improves performance by loading images only when they're visible
*/
class LazyLoader {
constructor() {
this.images = [];
this.observer = null;
this.init();
}
init() {
// Find all lazy images
this.images = document.querySelectorAll(
'img[data-src], img[loading="lazy"]'
);
// Set up Intersection Observer
if ("IntersectionObserver" in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
}
});
},
{
rootMargin: "50px",
}
);
this.images.forEach((img) => this.observer.observe(img));
} else {
// Fallback for older browsers
this.images.forEach((img) => this.loadImage(img));
}
}
loadImage(img) {
const src = img.getAttribute("data-src");
if (src) {
img.src = src;
img.removeAttribute("data-src");
}
// Add fade-in effect
img.addEventListener("load", () => {
img.classList.add("loaded");
});
if (this.observer) {
this.observer.unobserve(img);
}
}
// Add new images to observer
observe(images) {
if (!images) return;
const imageList = Array.isArray(images) ? images : [images];
imageList.forEach((img) => {
if (this.observer) {
this.observer.observe(img);
} else {
this.loadImage(img);
}
});
}
}
/**
* Smooth Scroll Handler
* Adds smooth scrolling to anchor links
*/
class SmoothScroll {
constructor() {
this.init();
}
init() {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
const href = anchor.getAttribute("href");
if (href === "#") return;
e.preventDefault();
const target = document.querySelector(href);
if (target) {
const offset = 80; // Account for fixed header
const top =
target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: "smooth",
});
// Update URL without scrolling
history.pushState(null, "", href);
}
});
});
}
}
/**
* Back to Top Button
* Shows/hides button based on scroll position
*/
class BackToTop {
constructor() {
this.button = null;
this.scrollThreshold = 300;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create button if it doesn't exist
this.button = document.getElementById("back-to-top");
if (!this.button) {
this.button = document.createElement("button");
this.button.id = "back-to-top";
this.button.className = "back-to-top";
this.button.innerHTML = "↑";
this.button.setAttribute("aria-label", "Back to top");
document.body.appendChild(this.button);
}
// Handle scroll
window.addEventListener("scroll", () => {
if (window.pageYOffset > this.scrollThreshold) {
this.button.classList.add("visible");
} else {
this.button.classList.remove("visible");
}
});
// Handle click
this.button.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
}
/**
* Loading Overlay
* Shows loading state during async operations
*/
class LoadingOverlay {
constructor() {
this.overlay = null;
this.activeOperations = 0;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create overlay if it doesn't exist
this.overlay = document.getElementById("loading-overlay");
if (!this.overlay) {
this.overlay = document.createElement("div");
this.overlay.id = "loading-overlay";
this.overlay.className = "loading-overlay";
this.overlay.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<p>Loading...</p>
</div>
`;
document.body.appendChild(this.overlay);
}
}
show() {
this.activeOperations++;
this.overlay.classList.add("active");
document.body.style.overflow = "hidden";
}
hide() {
this.activeOperations = Math.max(0, this.activeOperations - 1);
if (this.activeOperations === 0) {
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
// Force hide regardless of operation count
forceHide() {
this.activeOperations = 0;
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
/**
* Page Visibility Handler
* Handles actions when page becomes visible/hidden
*/
class PageVisibility {
constructor() {
this.callbacks = {
visible: [],
hidden: [],
};
this.init();
}
init() {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.callbacks.hidden.forEach((cb) => cb());
} else {
this.callbacks.visible.forEach((cb) => cb());
}
});
}
onVisible(callback) {
this.callbacks.visible.push(callback);
}
onHidden(callback) {
this.callbacks.hidden.push(callback);
}
}
/**
* Network Status Handler
* Monitors online/offline status
*/
class NetworkStatus {
constructor() {
this.isOnline = navigator.onLine;
this.callbacks = {
online: [],
offline: [],
};
this.init();
}
init() {
window.addEventListener("online", () => {
this.isOnline = true;
this.callbacks.online.forEach((cb) => cb());
this.showNotification("Back online", "success");
});
window.addEventListener("offline", () => {
this.isOnline = false;
this.callbacks.offline.forEach((cb) => cb());
this.showNotification("No internet connection", "error");
});
}
onOnline(callback) {
this.callbacks.online.push(callback);
}
onOffline(callback) {
this.callbacks.offline.push(callback);
}
showNotification(message, type) {
if (window.Utils && window.Utils.notify) {
window.Utils.notify(message, type);
}
}
}
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPageTransitions);
} else {
initPageTransitions();
}
function initPageTransitions() {
// Initialize all modules
window.pageTransitions = new PageTransitions();
window.lazyLoader = new LazyLoader();
window.smoothScroll = new SmoothScroll();
window.backToTop = new BackToTop();
window.loadingOverlay = new LoadingOverlay();
window.pageVisibility = new PageVisibility();
window.networkStatus = new NetworkStatus();
console.log("Page transitions initialized");
}
// Add CSS if not already present
if (!document.getElementById("page-transitions-styles")) {
const style = document.createElement("style");
style.id = "page-transitions-styles";
style.textContent = `
.page-transition {
opacity: 1;
transition: opacity 300ms ease;
}
.page-transition.fade-in {
opacity: 0;
pointer-events: none;
animation: fadeIn 300ms ease forwards;
}
.page-transition.fade-out {
opacity: 1;
animation: fadeOut 300ms ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
img[data-src] {
opacity: 0;
transition: opacity 300ms ease;
}
img.loaded {
opacity: 1;
}
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #667eea;
color: white;
border: none;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 999;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background: #5568d3;
transform: translateY(-2px);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.95);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9999;
}
.loading-overlay.active {
opacity: 1;
visibility: visible;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner p {
color: #667eea;
font-size: 16px;
font-weight: 600;
margin: 0;
}
`;
document.head.appendChild(style);
// Fade in page on load (if coming from a transition)
function initPageTransition() {
// Check if we're coming from a transition
const isTransitioning = sessionStorage.getItem("page-transitioning");
if (isTransitioning === "true") {
document.body.style.opacity = "0";
sessionStorage.removeItem("page-transitioning");
// Wait for content to be ready, then fade in
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.body.style.opacity = "1";
});
});
}
}
// Handle navigation with transitions
function setupNavigationTransitions() {
// Get all internal links
document.addEventListener("click", function (e) {
const link = e.target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
// Skip if:
// - External link
// - Opens in new tab
// - Has download attribute
// - Is a hash link on same page
// - Is a javascript: link
// - Is a mailto: or tel: link
if (
!href ||
link.target === "_blank" ||
link.hasAttribute("download") ||
href.startsWith("javascript:") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
href.startsWith("#") ||
(href.includes("://") && !href.includes(window.location.host))
) {
return;
}
// Prevent default navigation
e.preventDefault();
// Start transition
document.body.classList.add("page-transitioning");
sessionStorage.setItem("page-transitioning", "true");
// Navigate after fade-out completes
setTimeout(() => {
window.location.href = href;
}, 250); // Match CSS transition duration
});
}
// Use View Transitions API if available (Chrome 111+, Safari 18+)
function setupViewTransitions() {
if (!document.startViewTransition) return;
document.addEventListener(
"click",
function (e) {
const link = e.target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
// Same checks as above
if (
!href ||
link.target === "_blank" ||
link.hasAttribute("download") ||
href.startsWith("javascript:") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
href.startsWith("#") ||
(href.includes("://") && !href.includes(window.location.host))
) {
return;
}
e.preventDefault();
// Use View Transitions API for smooth cross-page transitions
sessionStorage.setItem("page-transitioning", "true");
document.startViewTransition(() => {
window.location.href = href;
});
},
true
); // Use capture to run before other handlers
}
// Initialize
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
initPageTransition();
setupNavigationTransitions();
});
} else {
initPageTransition();
setupNavigationTransitions();
}
// For browsers that support View Transitions API (progressive enhancement)
if ("startViewTransition" in document) {
const viewStyle = document.createElement("style");
viewStyle.textContent = `
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;
}
`;
document.head.appendChild(viewStyle);
}
})();
}

View File

@@ -1,376 +1,306 @@
/**
* Enhanced Cart and Wishlist Management System
* Amazon/eBay-style product display with images and details
* Shopping/Products Component
* Handles product display, filtering, and interactions
*/
class ShoppingManager {
constructor() {
this.cart = this.loadFromStorage("skyart_cart") || [];
this.wishlist = this.loadFromStorage("skyart_wishlist") || [];
this.init();
}
(function () {
"use strict";
init() {
this.updateAllBadges();
this.setupEventListeners();
this.renderCart();
this.renderWishlist();
}
class ShoppingPage {
constructor() {
this.productsContainer = document.getElementById("productsContainer");
this.loadingIndicator = document.getElementById("loadingIndicator");
this.errorContainer = document.getElementById("errorContainer");
this.currentCategory = window.Utils.getUrlParameter("category") || "all";
this.currentSort = "newest";
this.products = [];
loadFromStorage(key) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (e) {
console.error("Error loading from storage:", e);
return null;
this.init();
}
}
saveToStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (e) {
console.error("Error saving to storage:", e);
async init() {
this.setupEventListeners();
await this.loadProducts();
}
}
setupEventListeners() {
// Cart toggle
const cartToggle = document.getElementById("cartToggle");
const cartPanel = document.getElementById("cartPanel");
const cartClose = document.getElementById("cartClose");
if (cartToggle) {
cartToggle.addEventListener("click", (e) => {
e.stopPropagation();
cartPanel?.classList.toggle("active");
document.getElementById("wishlistPanel")?.classList.remove("active");
setupEventListeners() {
// Category filters
document.querySelectorAll("[data-category]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
this.currentCategory = e.currentTarget.dataset.category;
this.filterProducts();
});
});
}
if (cartClose) {
cartClose.addEventListener("click", (e) => {
e.stopPropagation();
cartPanel?.classList.remove("active");
});
}
// Wishlist toggle
const wishlistToggle = document.getElementById("wishlistToggle");
const wishlistPanel = document.getElementById("wishlistPanel");
const wishlistClose = document.getElementById("wishlistClose");
if (wishlistToggle) {
wishlistToggle.addEventListener("click", (e) => {
e.stopPropagation();
wishlistPanel?.classList.toggle("active");
cartPanel?.classList.remove("active");
});
}
if (wishlistClose) {
wishlistClose.addEventListener("click", (e) => {
e.stopPropagation();
wishlistPanel?.classList.remove("active");
});
}
// Mobile menu
const mobileToggle = document.getElementById("mobileMenuToggle");
const mobileMenu = document.getElementById("mobileMenu");
const mobileClose = document.getElementById("mobileMenuClose");
if (mobileToggle) {
mobileToggle.addEventListener("click", () => {
mobileMenu?.classList.toggle("active");
document.body.style.overflow = mobileMenu?.classList.contains("active")
? "hidden"
: "";
});
}
if (mobileClose) {
mobileClose.addEventListener("click", () => {
mobileMenu?.classList.remove("active");
document.body.style.overflow = "";
});
}
// Close dropdowns on outside click
document.addEventListener("click", (e) => {
if (!e.target.closest(".cart-dropdown-wrapper")) {
cartPanel?.classList.remove("active");
// Sort dropdown
const sortSelect = document.getElementById("sortSelect");
if (sortSelect) {
sortSelect.addEventListener("change", (e) => {
this.currentSort = e.target.value;
this.filterProducts();
});
}
if (!e.target.closest(".wishlist-dropdown-wrapper")) {
wishlistPanel?.classList.remove("active");
}
});
}
// Add to Cart
addToCart(product, quantity = 1) {
const existingItem = this.cart.find((item) => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cart.push({
id: product.id,
name: product.name,
price: parseFloat(product.price),
imageurl: product.imageurl,
quantity: quantity,
addedAt: new Date().toISOString(),
});
}
this.saveToStorage("skyart_cart", this.cart);
this.updateAllBadges();
this.renderCart();
this.showNotification(`${product.name} added to cart!`, "success");
}
// Remove from Cart
removeFromCart(productId) {
this.cart = this.cart.filter((item) => item.id !== productId);
this.saveToStorage("skyart_cart", this.cart);
this.updateAllBadges();
this.renderCart();
this.showNotification("Item removed from cart", "info");
}
// Update Cart Quantity
updateCartQuantity(productId, quantity) {
const item = this.cart.find((item) => item.id === productId);
if (item) {
if (quantity <= 0) {
this.removeFromCart(productId);
} else {
item.quantity = quantity;
this.saveToStorage("skyart_cart", this.cart);
this.updateAllBadges();
this.renderCart();
// Search
const searchInput = document.getElementById("productSearch");
if (searchInput) {
searchInput.addEventListener(
"input",
window.Utils.debounce((e) => {
this.searchProducts(e.target.value);
}, 300)
);
}
}
}
// Add to Wishlist
addToWishlist(product) {
const exists = this.wishlist.find((item) => item.id === product.id);
async loadProducts() {
if (!this.productsContainer) return;
if (!exists) {
this.wishlist.push({
id: product.id,
name: product.name,
price: parseFloat(product.price),
imageurl: product.imageurl,
addedAt: new Date().toISOString(),
try {
this.showLoading();
const response = await window.API.getProducts();
this.products = response.products || response.data || [];
this.renderProducts(this.products);
this.hideLoading();
} catch (error) {
console.error("Error loading products:", error);
this.showError("Failed to load products. Please try again later.");
this.hideLoading();
}
}
filterProducts() {
let filtered = [...this.products];
// Filter by category
if (this.currentCategory && this.currentCategory !== "all") {
filtered = filtered.filter(
(p) =>
p.category?.toLowerCase() === this.currentCategory.toLowerCase()
);
}
// Sort products
filtered = this.sortProducts(filtered);
this.renderProducts(filtered);
}
sortProducts(products) {
switch (this.currentSort) {
case "price-low":
return products.sort((a, b) => (a.price || 0) - (b.price || 0));
case "price-high":
return products.sort((a, b) => (b.price || 0) - (a.price || 0));
case "name":
return products.sort((a, b) =>
(a.title || a.name || "").localeCompare(b.title || b.name || "")
);
case "newest":
default:
return products.sort(
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
);
}
}
searchProducts(query) {
if (!query.trim()) {
this.filterProducts();
return;
}
const searchTerm = query.toLowerCase();
const filtered = this.products.filter((p) => {
const title = (p.title || p.name || "").toLowerCase();
const description = (p.description || "").toLowerCase();
const category = (p.category || "").toLowerCase();
return (
title.includes(searchTerm) ||
description.includes(searchTerm) ||
category.includes(searchTerm)
);
});
this.saveToStorage("skyart_wishlist", this.wishlist);
this.updateAllBadges();
this.renderWishlist();
this.showNotification(`${product.name} added to wishlist!`, "success");
} else {
this.showNotification("Already in wishlist", "info");
}
}
// Remove from Wishlist
removeFromWishlist(productId) {
this.wishlist = this.wishlist.filter((item) => item.id !== productId);
this.saveToStorage("skyart_wishlist", this.wishlist);
this.updateAllBadges();
this.renderWishlist();
this.showNotification("Item removed from wishlist", "info");
}
// Move from Wishlist to Cart
moveToCart(productId) {
const item = this.wishlist.find((item) => item.id === productId);
if (item) {
this.addToCart(item, 1);
this.removeFromWishlist(productId);
}
}
// Update All Badges
updateAllBadges() {
const cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
const wishlistCount = this.wishlist.length;
const cartBadge = document.getElementById("cartCount");
const wishlistBadge = document.getElementById("wishlistCount");
if (cartBadge) {
cartBadge.textContent = cartCount;
cartBadge.style.display = cartCount > 0 ? "flex" : "none";
this.renderProducts(filtered);
}
if (wishlistBadge) {
wishlistBadge.textContent = wishlistCount;
wishlistBadge.style.display = wishlistCount > 0 ? "flex" : "none";
}
}
renderProducts(products) {
if (!this.productsContainer) return;
// Render Cart
renderCart() {
const cartContent = document.getElementById("cartContent");
const cartSubtotal = document.getElementById("cartSubtotal");
if (products.length === 0) {
this.productsContainer.innerHTML = `
<div class="no-products">
<i class="bi bi-inbox" style="font-size: 48px; opacity: 0.5;"></i>
<p>No products found</p>
</div>
`;
return;
}
if (!cartContent) return;
const html = products
.map((product) => this.renderProductCard(product))
.join("");
this.productsContainer.innerHTML = html;
if (this.cart.length === 0) {
cartContent.innerHTML = '<p class="empty-state">Your cart is empty</p>';
if (cartSubtotal) cartSubtotal.textContent = "$0.00";
return;
// Setup product card listeners
this.setupProductListeners();
}
const subtotal = this.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
renderProductCard(product) {
const id = product.id;
const title = window.Utils?.escapeHtml
? window.Utils.escapeHtml(product.title || product.name || "Product")
: product.title || product.name || "Product";
const price = window.Utils?.formatCurrency
? window.Utils.formatCurrency(product.price || 0)
: `$${parseFloat(product.price || 0).toFixed(2)}`;
cartContent.innerHTML = this.cart
.map(
(item) => `
<div class="cart-item" data-product-id="${item.id}">
<div class="cart-item-image">
<img src="${item.imageurl || "/assets/images/placeholder.jpg"}"
alt="${item.name}"
onerror="this.src='/assets/images/placeholder.jpg'" />
</div>
<div class="cart-item-details">
<h4 class="cart-item-name">${item.name}</h4>
<p class="cart-item-price">$${item.price.toFixed(2)}</p>
<div class="cart-item-quantity">
<button class="qty-btn" onclick="shoppingManager.updateCartQuantity('${
item.id
}', ${item.quantity - 1})">
<i class="bi bi-dash"></i>
</button>
<span class="qty-value">${item.quantity}</span>
<button class="qty-btn" onclick="shoppingManager.updateCartQuantity('${
item.id
}', ${item.quantity + 1})">
<i class="bi bi-plus"></i>
// Get image URL from multiple possible sources
let imageUrl = "/assets/images/placeholder.jpg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
const primaryImg = product.images.find((img) => img.is_primary);
imageUrl = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageUrl) {
imageUrl = product.imageUrl;
} else if (product.image_url) {
imageUrl = product.image_url;
}
// Get description
const description =
product.shortdescription ||
(product.description
? product.description.substring(0, 100) + "..."
: "");
const isInWishlist = window.AppState?.isInWishlist(id) || false;
return `
<article class="product-card" data-id="${id}">
<div class="product-image-wrapper">
<img src="${imageUrl}" alt="${title}" class="product-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
<button
class="wishlist-btn ${isInWishlist ? "active" : ""}"
data-id="${id}"
aria-label="${
isInWishlist ? "Remove from wishlist" : "Add to wishlist"
}"
>
<i class="bi bi-heart${isInWishlist ? "-fill" : ""}"></i>
</button>
</div>
</div>
<div class="cart-item-actions">
<button class="cart-item-remove" onclick="shoppingManager.removeFromCart('${
item.id
}')" title="Remove">
<i class="bi bi-trash"></i>
</button>
<p class="cart-item-total">$${(item.price * item.quantity).toFixed(
2
)}</p>
</div>
</div>
`
)
.join("");
<div class="product-info">
<a href="/product?id=${id}" style="text-decoration: none; color: inherit;">
<h3 class="product-title">${title}</h3>
</a>
${
description
? `<div class="product-description">${description}</div>`
: ""
}
<p class="product-price">${price}</p>
<div class="product-actions">
<button class="btn-add-to-cart" data-id="${id}" style="flex: 1;">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</div>
</div>
</article>
`;
}
if (cartSubtotal) {
cartSubtotal.textContent = `$${subtotal.toFixed(2)}`;
setupProductListeners() {
// Add to cart buttons
this.productsContainer
.querySelectorAll(".btn-add-to-cart")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
window.AppState.addToCart(product);
}
});
});
// Wishlist buttons
this.productsContainer
.querySelectorAll(".wishlist-btn")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
if (window.AppState.isInWishlist(id)) {
window.AppState.removeFromWishlist(id);
} else {
window.AppState.addToWishlist(product);
}
this.renderProducts(this.products);
}
});
});
}
showLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "flex";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "0.5";
}
}
hideLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "none";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "1";
}
}
showError(message) {
if (this.errorContainer) {
this.errorContainer.innerHTML = `
<div class="error-message" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<p>${window.Utils.escapeHtml(message)}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
this.errorContainer.style.display = "block";
}
}
}
// Render Wishlist
renderWishlist() {
const wishlistContent = document.getElementById("wishlistContent");
if (!wishlistContent) return;
if (this.wishlist.length === 0) {
wishlistContent.innerHTML =
'<p class="empty-state">Your wishlist is empty</p>';
return;
// Initialize on shop/products pages
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
});
} else {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
wishlistContent.innerHTML = this.wishlist
.map(
(item) => `
<div class="wishlist-item" data-product-id="${item.id}">
<div class="wishlist-item-image">
<img src="${item.imageurl || "/assets/images/placeholder.jpg"}"
alt="${item.name}"
onerror="this.src='/assets/images/placeholder.jpg'" />
</div>
<div class="wishlist-item-details">
<h4 class="wishlist-item-name">${item.name}</h4>
<p class="wishlist-item-price">$${item.price.toFixed(2)}</p>
<button class="btn-move-to-cart" onclick="shoppingManager.moveToCart('${
item.id
}')">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</div>
<button class="wishlist-item-remove" onclick="shoppingManager.removeFromWishlist('${
item.id
}')" title="Remove">
<i class="bi bi-x-lg"></i>
</button>
</div>
`
)
.join("");
}
// Show Notification
showNotification(message, type = "info") {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="bi bi-${
type === "success" ? "check-circle" : "info-circle"
}"></i>
<span>${message}</span>
`;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add("show"), 10);
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Get Cart Total
getCartTotal() {
return this.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Get Cart Count
getCartCount() {
return this.cart.reduce((sum, item) => sum + item.quantity, 0);
}
// Clear Cart
clearCart() {
this.cart = [];
this.saveToStorage("skyart_cart", this.cart);
this.updateAllBadges();
this.renderCart();
}
}
// Initialize Shopping Manager
const shoppingManager = new ShoppingManager();
// Make it globally available
window.shoppingManager = shoppingManager;
// Navigation active state
document.addEventListener("DOMContentLoaded", () => {
const currentPage = window.location.pathname.split("/").pop() || "home.html";
document.querySelectorAll(".nav-link, .mobile-link").forEach((link) => {
const linkPage = link.getAttribute("href")?.split("/").pop();
if (linkPage === currentPage) {
link.classList.add("active");
}
});
});
})();

View File

@@ -0,0 +1,236 @@
/**
* Global State Management
* Centralized state for cart, wishlist, and user preferences
*/
(function () {
"use strict";
class StateManager {
constructor() {
this.state = {
cart: [],
wishlist: [],
user: null,
preferences: {},
};
this.listeners = {};
this.init();
}
init() {
this.loadFromStorage();
this.setupStorageSync();
}
loadFromStorage() {
try {
this.state.cart = JSON.parse(localStorage.getItem("cart") || "[]");
this.state.wishlist = JSON.parse(
localStorage.getItem("wishlist") || "[]"
);
this.state.preferences = JSON.parse(
localStorage.getItem("preferences") || "{}"
);
} catch (e) {
console.error("State load error:", e);
}
}
saveToStorage() {
try {
localStorage.setItem("cart", JSON.stringify(this.state.cart));
localStorage.setItem("wishlist", JSON.stringify(this.state.wishlist));
localStorage.setItem(
"preferences",
JSON.stringify(this.state.preferences)
);
} catch (e) {
console.error("State save error:", e);
}
}
setupStorageSync() {
window.addEventListener("storage", (e) => {
if (e.key === "cart" || e.key === "wishlist") {
this.loadFromStorage();
this.emit("stateChanged", { key: e.key });
}
});
}
// Cart methods
addToCart(product, quantity = 1) {
const existing = this.state.cart.find((item) => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.state.cart.push({
...product,
quantity,
addedAt: Date.now(),
});
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
return this.state.cart;
}
removeFromCart(productId) {
this.state.cart = this.state.cart.filter((item) => item.id !== productId);
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
return this.state.cart;
}
updateCartQuantity(productId, quantity) {
const item = this.state.cart.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
return this.removeFromCart(productId);
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
}
return this.state.cart;
}
getCart() {
return this.state.cart;
}
getCartTotal() {
return this.state.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
getCartCount() {
return this.state.cart.reduce((sum, item) => sum + item.quantity, 0);
}
clearCart() {
this.state.cart = [];
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
}
// Wishlist methods
addToWishlist(product) {
const exists = this.state.wishlist.find((item) => item.id === product.id);
if (!exists) {
this.state.wishlist.push({
...product,
addedAt: Date.now(),
});
this.saveToStorage();
this.emit("wishlistUpdated", this.state.wishlist);
return true;
}
return false;
}
removeFromWishlist(productId) {
this.state.wishlist = this.state.wishlist.filter(
(item) => item.id !== productId
);
this.saveToStorage();
this.emit("wishlistUpdated", this.state.wishlist);
return this.state.wishlist;
}
getWishlist() {
return this.state.wishlist;
}
isInWishlist(productId) {
return this.state.wishlist.some((item) => item.id === productId);
}
// Event system
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(
(cb) => cb !== callback
);
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => {
try {
callback(data);
} catch (e) {
console.error(`Error in ${event} listener:`, e);
}
});
}
}
}
// Create global instance
window.StateManager = window.StateManager || new StateManager();
// Expose helper functions for backward compatibility
window.addToCart = function (productId, name, price, imageurl) {
const product = { id: productId, name, price: parseFloat(price), imageurl };
window.StateManager.addToCart(product, 1);
if (window.showNotification) {
window.showNotification(`${name} added to cart!`, "success");
}
};
window.addToWishlist = function (productId, name, price, imageurl) {
const product = { id: productId, name, price: parseFloat(price), imageurl };
const added = window.StateManager.addToWishlist(product);
if (window.showNotification) {
window.showNotification(
added ? `${name} added to wishlist!` : "Already in wishlist!",
added ? "success" : "info"
);
}
};
// Update badges on state changes
window.StateManager.on("cartUpdated", () => {
const badge = document.querySelector(".cart-badge");
if (badge) {
const count = window.StateManager.getCartCount();
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
});
window.StateManager.on("wishlistUpdated", () => {
const badge = document.querySelector(".wishlist-badge");
if (badge) {
const count = window.StateManager.getWishlist().length;
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
});
// Initialize badges
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.StateManager.emit("cartUpdated");
window.StateManager.emit("wishlistUpdated");
});
} else {
window.StateManager.emit("cartUpdated");
window.StateManager.emit("wishlistUpdated");
}
})();

View File

@@ -12,44 +12,46 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1767233028" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/responsive.css" />
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link active">About</a>
<a href="/about" class="nav-link active">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
<a href="/blog" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
@@ -75,7 +77,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -104,10 +106,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -122,18 +124,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -213,7 +215,7 @@
/* Team Section Styles */
.team-section {
padding: 80px 0;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
background: #fcb1d8;
}
.team-header {
@@ -224,17 +226,14 @@
.section-title {
font-size: 2.5rem;
font-weight: 700;
color: #2d3748;
color: #202023;
margin-bottom: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-subtitle {
font-size: 1.125rem;
color: #718096;
color: #202023;
opacity: 0.8;
max-width: 600px;
margin: 0 auto;
}
@@ -252,10 +251,11 @@
border-radius: 20px;
padding: 40px 30px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
box-shadow: 0 4px 15px rgba(252, 177, 216, 0.3);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
border: 2px solid #ffd0d0;
}
.team-card::before {
@@ -265,14 +265,15 @@
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
background: #fcb1d8;
transform: scaleX(0);
transition: transform 0.4s ease;
}
.team-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.25);
box-shadow: 0 20px 40px rgba(252, 177, 216, 0.4);
border-color: #fcb1d8;
}
.team-card:hover::before {
@@ -291,18 +292,18 @@
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 5px solid #667eea;
border: 5px solid #fcb1d8;
transition: all 0.4s ease;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #fcb1d8;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
box-shadow: 0 8px 20px rgba(252, 177, 216, 0.4);
}
.team-card:hover .team-image {
transform: scale(1.1) rotate(5deg);
border-color: #764ba2;
border-color: #f6ccde;
}
.team-image img {
@@ -320,18 +321,18 @@
.team-name {
font-size: 1.5rem;
font-weight: 700;
color: #2d3748;
color: #202023;
margin-bottom: 8px;
transition: color 0.3s ease;
}
.team-card:hover .team-name {
color: #667eea;
color: #fcb1d8;
}
.team-position {
font-size: 1.125rem;
color: #667eea;
color: #fcb1d8;
font-weight: 600;
margin-bottom: 15px;
text-transform: uppercase;
@@ -341,7 +342,8 @@
.team-bio {
font-size: 1rem;
color: #718096;
color: #202023;
opacity: 0.8;
line-height: 1.7;
margin-bottom: 0;
}
@@ -394,28 +396,28 @@
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
@@ -425,7 +427,8 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>

View File

@@ -0,0 +1,458 @@
/* Import Amsterdam Three Font */
@font-face {
font-family: 'Amsterdam Three';
src: url('/assets/fonts/AmsterdamThreeSlant-axaym.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Modern Navbar Styles */
.modern-navbar {
position: sticky;
top: 0;
z-index: 1000;
background: #FFD0D0;
box-shadow: none;
font-family: 'Roboto', sans-serif;
}
.navbar-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 0 24px;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
height: 72px;
}
/* Logo Section */
.navbar-brand {
flex-shrink: 0 !important;
min-width: 240px !important;
margin-right: 48px !important;
}
.brand-link {
display: flex !important;
align-items: center !important;
gap: 20px !important;
text-decoration: none;
transition: opacity 0.2s;
}
.brand-link:hover {
opacity: 0.8;
}
.brand-logo {
width: 56px;
height: 56px;
object-fit: contain;
border-radius: 8px;
}
.brand-name {
font-family: 'Amsterdam Three', cursive;
font-size: 20px;
font-weight: 400;
color: #202023;
letter-spacing: 0.5px;
white-space: nowrap;
}
/* Main Navigation */
.navbar-menu {
flex: 1 !important;
display: flex !important;
justify-content: center !important;
padding: 0 60px !important;
min-width: 0 !important;
}
.nav-menu-list {
display: flex !important;
align-items: center !important;
gap: 8px !important;
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
margin: 0;
}
.nav-link {
display: block;
padding: 10px 20px;
font-size: 15px;
font-weight: 500;
color: #202023;
text-decoration: none;
border-radius: 6px;
transition: all 0.2s;
letter-spacing: 0.3px;
}
.nav-link:hover,
.nav-link.active {
color: #202023;
background: #FCB1D8;
}
/* Right Actions */
.navbar-actions {
display: flex !important;
align-items: center !important;
gap: 16px !important;
flex-shrink: 0 !important;
min-width: 120px !important;
justify-content: flex-end !important;
margin-left: 48px !important;
}
.action-item {
position: relative;
}
.action-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: none;
background: transparent;
color: #202023;
font-size: 22px;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: #FFEBEB;
color: #202023;
}
.action-badge {
position: absolute;
top: 6px;
right: 6px;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: #FCB1D8;
color: #202023;
font-size: 11px;
font-weight: 600;
border-radius: 9px;
display: none;
align-items: center;
justify-content: center;
}
.action-badge.show {
display: flex;
}
/* Dropdown Styles */
.action-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 380px;
max-height: 500px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
display: none;
flex-direction: column;
z-index: 1001;
}
.action-dropdown.active {
display: flex;
}
.dropdown-head {
padding: 20px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.dropdown-head h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.dropdown-close {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: #6b7280;
font-size: 18px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.dropdown-close:hover {
background: #f3f4f6;
color: #1a1a1a;
}
.dropdown-body {
flex: 1;
overflow-y: auto;
padding: 16px;
max-height: 350px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
font-size: 15px;
}
.dropdown-foot {
padding: 16px 20px;
border-top: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
gap: 12px;
}
.cart-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.summary-label {
font-size: 15px;
font-weight: 500;
color: #6b7280;
}
.summary-value {
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
}
/* Buttons */
.btn-primary-full,
.btn-outline,
.btn-text {
display: block;
text-align: center;
padding: 12px 20px;
font-size: 15px;
font-weight: 500;
text-decoration: none;
border-radius: 8px;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.btn-primary-full {
background: #6b46c1;
color: white;
}
.btn-primary-full:hover {
background: #5936a3;
}
.btn-outline {
background: transparent;
color: #6b46c1;
border: 1px solid #6b46c1;
}
.btn-outline:hover {
background: #f3f0ff;
}
.btn-text {
background: transparent;
color: #6b7280;
padding: 8px;
}
.btn-text:hover {
color: #1a1a1a;
}
/* Mobile Toggle */
.mobile-toggle {
display: none;
flex-direction: column;
gap: 5px;
width: 44px;
height: 44px;
padding: 10px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s;
}
.mobile-toggle:hover {
background: #f5f5f5;
}
.toggle-line {
width: 100%;
height: 2px;
background: #4a4a4a;
border-radius: 2px;
transition: all 0.3s;
}
/* Mobile Menu */
.mobile-menu {
position: fixed;
top: 0;
right: -100%;
width: 320px;
height: 100vh;
background: white;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.1);
z-index: 1002;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
.mobile-menu.active {
right: 0;
}
.mobile-menu-header {
padding: 24px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.mobile-brand {
font-family: 'Amsterdam Three', cursive;
font-size: 22px;
font-weight: 400;
color: #1a1a1a;
}
.mobile-close {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: #6b7280;
font-size: 20px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-close:hover {
background: #f3f4f6;
}
.mobile-menu-list {
list-style: none;
margin: 0;
padding: 16px;
}
.mobile-menu-list li {
margin-bottom: 4px;
}
.mobile-link {
display: block;
padding: 14px 16px;
font-size: 16px;
font-weight: 500;
color: #4a4a4a;
text-decoration: none;
border-radius: 8px;
transition: all 0.2s;
}
.mobile-link:hover,
.mobile-link.active {
color: #6b46c1;
background: #f3f0ff;
}
/* Responsive Design */
@media (max-width: 1024px) {
.navbar-menu {
display: none;
}
.mobile-toggle {
display: flex;
}
.navbar-brand {
min-width: auto;
margin-right: auto;
}
.navbar-actions {
margin-left: 16px;
}
}
@media (max-width: 640px) {
.navbar-wrapper {
padding: 0 16px;
height: 64px;
}
.brand-name {
font-size: 18px;
}
.brand-logo {
width: 44px;
height: 44px;
}
.navbar-brand {
min-width: auto;
margin-right: 12px;
}
.navbar-actions {
margin-left: 12px;
gap: 8px;
}
.action-dropdown {
width: 100vw;
max-width: 380px;
right: -16px;
}
}

View File

@@ -0,0 +1,439 @@
/**
* Enhanced Responsive Utilities
* Comprehensive responsive design system with accessibility
*/
/* ========================================
LOADING STATES
======================================== */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ========================================
PRODUCT GRID RESPONSIVE
======================================== */
.products-grid {
display: grid;
gap: 24px;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
}
@media (min-width: 768px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}
@media (min-width: 1024px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
gap: 28px;
}
}
@media (min-width: 1280px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
}
/* ========================================
PRODUCT CARD RESPONSIVE
======================================== */
.product-card {
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.15);
transition: all 0.3s ease;
}
.product-card:hover {
box-shadow: 0 4px 16px rgba(252, 177, 216, 0.25);
transform: translateY(-4px);
}
.product-image {
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
border-radius: 0;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.product-card:hover .product-image img {
transform: scale(1.05);
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 8px;
}
.product-info h3 {
font-size: 16px;
font-weight: 600;
margin: 0;
line-height: 1.4;
color: #202023;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-description {
font-size: 14px;
color: #202023;
opacity: 0.7;
margin: 0;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex: 1;
min-height: 42px;
}
.product-card .price {
font-size: 20px;
font-weight: 700;
color: #FCB1D8;
margin: 0;
}
.product-actions {
display: flex;
gap: 8px;
padding: 0 16px 16px 16px;
margin-top: auto;
}
.product-actions .btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: #FCB1D8;
color: #202023;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.product-actions .btn:hover {
background: #F6CCDE;
transform: translateY(-2px);
}
.product-actions .btn i {
font-size: 18px;
}
@media (max-width: 639px) {
.product-info h3 {
font-size: 14px;
}
.product-description {
font-size: 13px;
-webkit-line-clamp: 2;
}
.product-card .price {
font-size: 18px;
}
}
/* ========================================
NAVBAR RESPONSIVE
======================================== */
.modern-navbar {
padding: 0 20px;
}
@media (min-width: 768px) {
.modern-navbar {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.modern-navbar {
padding: 0 60px;
}
}
.navbar-brand {
min-width: 200px;
}
@media (max-width: 767px) {
.navbar-brand {
min-width: 150px;
}
.navbar-menu {
display: none;
}
}
/* ========================================
MOBILE MENU
======================================== */
.mobile-menu {
position: fixed;
top: 0;
left: -100%;
width: 280px;
height: 100vh;
background: white;
z-index: 9999;
transition: left 0.3s ease;
overflow-y: auto;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.mobile-menu.active {
left: 0;
}
.mobile-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 9998;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-menu.active ~ .mobile-menu-overlay,
.mobile-menu-overlay.active {
opacity: 1;
visibility: visible;
}
@media (min-width: 768px) {
.mobile-menu-toggle {
display: none;
}
}
/* ========================================
BUTTONS RESPONSIVE
======================================== */
.btn {
padding: 10px 20px;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s;
}
.btn-small {
padding: 8px 16px;
font-size: 13px;
}
.btn-icon {
width: 40px;
height: 40px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
@media (max-width: 639px) {
.btn {
padding: 8px 16px;
font-size: 13px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.btn-icon {
width: 36px;
height: 36px;
}
}
/* ========================================
UTILITY CLASSES
======================================== */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
@media (min-width: 768px) {
.container {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.container {
padding: 0 60px;
}
}
/* Text utilities */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
/* Display utilities */
.hidden { display: none !important; }
.block { display: block !important; }
.inline-block { display: inline-block !important; }
.flex { display: flex !important; }
.inline-flex { display: inline-flex !important; }
/* Responsive visibility */
@media (max-width: 639px) {
.hidden-mobile { display: none !important; }
}
@media (min-width: 640px) and (max-width: 767px) {
.hidden-tablet { display: none !important; }
}
@media (min-width: 768px) {
.hidden-desktop { display: none !important; }
}
@media (max-width: 639px) {
.visible-mobile { display: block !important; }
}
@media (min-width: 640px) and (max-width: 767px) {
.visible-tablet { display: block !important; }
}
@media (min-width: 768px) {
.visible-desktop { display: block !important; }
}
/* ========================================
ACCESSIBILITY
======================================== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border-width: 0;
}
.skip-link {
position: fixed;
top: -100px;
left: 10px;
background: #667eea;
color: white;
padding: 10px 20px;
border-radius: 4px;
text-decoration: none;
z-index: 10001;
transition: top 0.2s;
}
.skip-link:focus {
top: 10px;
outline: 2px solid white;
outline-offset: 2px;
}
*:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* ========================================
PRINT STYLES
======================================== */
@media print {
.modern-navbar,
.mobile-menu,
.notification-container,
.btn,
footer {
display: none !important;
}
body {
font-size: 12pt;
line-height: 1.5;
}
.product-card {
page-break-inside: avoid;
}
}

View File

@@ -0,0 +1,462 @@
/* Sky Art Shop - Color Palette */
:root {
/* Primary Color Palette */
--color-bg-main: #FFEBEB; /* Main background - light pink */
--color-bg-secondary: #FFD0D0; /* Secondary sections, navbar - medium pink */
--color-bg-promotion: #F6CCDE; /* Promotional sections - rosy pink */
--color-accent: #FCB1D8; /* Buttons, CTAs, separators - bright pink */
--color-text-main: #202023; /* Main text color - dark charcoal */
--color-text-light: #ffffff; /* Light text for dark backgrounds */
/* Gradients */
--gradient-primary: linear-gradient(135deg, #FFD0D0 0%, #FCB1D8 100%);
--gradient-soft: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%);
--gradient-promo: linear-gradient(135deg, #F6CCDE 0%, #FCB1D8 100%);
--gradient-hero: linear-gradient(135deg, #FFD0D0 0%, #F6CCDE 50%, #FCB1D8 100%);
/* Button States */
--btn-primary-bg: #FCB1D8;
--btn-primary-hover: #F6CCDE;
--btn-primary-text: #202023;
/* Shadows */
--shadow-sm: 0 2px 8px rgba(252, 177, 216, 0.15);
--shadow-md: 0 4px 16px rgba(252, 177, 216, 0.2);
--shadow-lg: 0 8px 24px rgba(252, 177, 216, 0.25);
}
/* Global Body Styles */
body {
background-color: var(--color-bg-main) !important;
color: var(--color-text-main) !important;
font-family: 'Roboto', sans-serif;
}
/* Typography - All headings use main text color */
h1, h2, h3, h4, h5, h6 {
color: var(--color-text-main) !important;
}
/* Ensure all paragraphs and text use main color */
p, span, div, li, td, th, label {
color: var(--color-text-main);
}
/* Primary Buttons and CTAs */
.btn-primary,
.action-btn,
.modern-btn,
.shop-now-btn,
.btn,
.hero .btn,
.cta-btn,
button[type="submit"] {
background: var(--btn-primary-bg) !important;
color: var(--color-text-main) !important;
border: none !important;
transition: all 0.3s ease !important;
font-weight: 600 !important;
}
.btn-primary:hover,
.action-btn:hover,
.modern-btn:hover,
.shop-now-btn:hover,
.btn:hover,
.hero .btn:hover,
.cta-btn:hover,
button[type="submit"]:hover {
background: var(--btn-primary-hover) !important;
color: var(--color-text-main) !important;
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* Navigation Bar */
.modern-navbar {
background: var(--color-bg-secondary) !important;
box-shadow: var(--shadow-sm) !important;
}
.brand-name {
color: var(--color-text-main) !important;
}
.nav-link {
color: var(--color-text-main) !important;
}
.nav-link:hover,
.nav-link.active {
background: var(--color-accent) !important;
color: var(--color-text-main) !important;
}
/* Hero Section */
.hero {
background: #FFEBEB !important;
}
.hero h1,
.hero h2,
.hero h3 {
color: var(--color-text-main) !important;
}
.hero p {
color: var(--color-text-main) !important;
opacity: 0.9;
}
/* Sections */
.inspiration,
.promotion-section,
.promo-section,
section[id*="promotion"],
.featured-section {
background: var(--color-bg-promotion) !important;
}
.featured,
.features,
.about-section {
background: var(--color-bg-secondary) !important;
}
/* Section Separators */
.section-separator,
.divider,
hr {
border-color: var(--color-accent) !important;
background: var(--color-accent) !important;
}
/* Cards and Containers */
.card,
.product-card,
.info-card,
.content-card {
background: var(--color-text-light) !important;
box-shadow: var(--shadow-sm) !important;
border: none !important;
}
.card:hover,
.product-card:hover {
box-shadow: var(--shadow-md) !important;
}
/* Card Headings */
.card h3,
.card h4,
.product-card h3,
.product-card h4 {
color: var(--color-text-main) !important;
}
/* Footer */
footer,
.footer {
background: var(--color-text-main) !important;
color: var(--color-text-light) !important;
}
footer h3,
footer h4,
footer p,
footer span {
color: var(--color-text-light) !important;
}
footer a {
color: var(--color-text-light) !important;
}
footer a:hover {
color: var(--color-accent) !important;
}
/* Badges */
.badge,
.action-badge,
.tag {
background: var(--color-accent) !important;
color: var(--color-text-main) !important;
}
/* Forms */
input,
textarea,
select {
border-color: var(--color-bg-secondary) !important;
background: var(--color-text-light) !important;
color: var(--color-text-main) !important;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--color-accent) !important;
box-shadow: 0 0 0 3px rgba(252, 177, 216, 0.1) !important;
}
input::placeholder,
textarea::placeholder {
color: var(--color-text-main);
opacity: 0.5;
}
/* Links */
a {
color: var(--color-text-main);
text-decoration: none;
}
a:hover {
color: var(--color-accent);
}
/* Contact Gradient Cards */
.gradient-card {
background: var(--gradient-primary) !important;
color: var(--color-text-main) !important;
}
.gradient-card h3,
.gradient-card h4,
.gradient-card p {
color: var(--color-text-main) !important;
}
.gradient-card i {
color: var(--color-text-main) !important;
}
/* Shipping/Top Banner */
.shipping-banner,
.top-banner,
.promo-banner {
background: var(--gradient-promo) !important;
color: var(--color-text-main) !important;
}
/* Product Grid */
.product-grid {
background: transparent;
}
/* Shop Page Backgrounds */
.utility-bar {
background: var(--color-bg-secondary) !important;
border-bottom: 1px solid var(--color-accent) !important;
}
.utility-bar h1 {
color: var(--color-text-main) !important;
}
.utility-bar p {
color: var(--color-text-main) !important;
opacity: 0.8;
}
/* Dropdown Panels */
.action-dropdown,
.cart-dropdown,
.wishlist-dropdown {
background: var(--color-text-light) !important;
box-shadow: var(--shadow-lg) !important;
}
.dropdown-head {
background: var(--color-bg-secondary) !important;
color: var(--color-text-main) !important;
}
.dropdown-head h3 {
color: var(--color-text-main) !important;
}
/* Loading States */
.skeleton,
.loading {
background: linear-gradient(90deg, var(--color-bg-main) 25%, var(--color-bg-secondary) 50%, var(--color-bg-main) 75%) !important;
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Alert/Notification Colors */
.alert-success,
.notification,
.toast {
background: var(--color-bg-secondary) !important;
color: var(--color-text-main) !important;
border: 1px solid var(--color-accent) !important;
}
/* Pagination */
.pagination .active {
background: var(--btn-primary-bg) !important;
color: var(--color-text-main) !important;
}
.pagination button:hover {
background: var(--btn-primary-hover) !important;
color: var(--color-text-main) !important;
}
/* Tabs */
.tab-active,
.active-tab {
background: var(--color-bg-secondary) !important;
color: var(--color-text-main) !important;
}
/* Price Tags */
.price,
.product-price {
color: var(--color-accent) !important;
font-weight: 700;
}
/* Status Indicators */
.status-active,
.in-stock {
color: var(--color-accent) !important;
}
/* Hover Effects */
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
/* Border Colors */
.border-primary {
border-color: var(--color-bg-secondary) !important;
}
.border-accent {
border-color: var(--color-accent) !important;
}
/* Mobile Menu */
.mobile-menu {
background: var(--color-bg-secondary) !important;
}
.mobile-brand {
color: var(--color-text-main) !important;
}
.mobile-link {
color: var(--color-text-main) !important;
}
.mobile-link:hover {
background: var(--color-accent) !important;
color: var(--color-text-main) !important;
}
/* Table Headers */
table thead {
background: var(--color-bg-secondary) !important;
}
table th {
color: var(--color-text-main) !important;
}
/* Filters and Sidebar */
.filter-section,
.sidebar {
background: var(--color-text-light) !important;
}
.filter-title {
color: var(--color-text-main) !important;
}
/* Search Bar */
.search-container input {
background: var(--color-text-light) !important;
border-color: var(--color-bg-secondary) !important;
color: var(--color-text-main) !important;
}
.search-container input:focus {
border-color: var(--color-accent) !important;
box-shadow: 0 0 0 3px rgba(252, 177, 216, 0.1) !important;
}
.search-container button {
background: var(--color-accent) !important;
color: var(--color-text-main) !important;
}
.search-container button:hover {
background: var(--btn-primary-hover) !important;
}
/* Modal/Dialog */
.modal,
.dialog {
background: var(--color-text-light) !important;
}
.modal-header {
background: var(--color-bg-secondary) !important;
color: var(--color-text-main) !important;
}
/* Breadcrumbs */
.breadcrumb {
background: transparent;
}
.breadcrumb a {
color: var(--color-text-main) !important;
}
.breadcrumb a:hover {
color: var(--color-accent) !important;
}
/* Empty States */
.empty-state {
color: var(--color-text-main) !important;
opacity: 0.6;
}
/* About, Blog Hero Sections */
.about-hero {
background: linear-gradient(135deg, #F6CCDE 0%, #FCB1D8 100%) !important;
padding: 40px 0 30px !important;
color: #202023 !important;
text-align: center;
}
.about-hero h1 {
font-size: 2.5rem !important;
margin-bottom: 12px !important;
font-weight: 700 !important;
color: #202023 !important;
}
.about-hero .hero-subtitle,
.about-hero p {
font-size: 1.1rem !important;
color: #202023 !important;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
.about-content {
background: #FFEBEB !important;
}

View File

@@ -0,0 +1,4 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="400" fill="#e5e7eb"/>
<text x="50%" y="50%" font-family="Arial" font-size="24" fill="#6b7280" text-anchor="middle" dominant-baseline="middle">No Image</text>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,111 @@
/**
* API Client
* Centralized API communication with error handling
*/
(function () {
"use strict";
class APIClient {
constructor(baseURL = "") {
this.baseURL = baseURL;
this.defaultHeaders = {
"Content-Type": "application/json",
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers,
},
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error(`API Error (${endpoint}):`, error);
throw error;
}
}
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: "GET" });
}
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, { method: "DELETE" });
}
// Product endpoints
async getProducts(params = {}) {
return this.get("/api/products", params);
}
async getProduct(id) {
return this.get(`/api/products/${id}`);
}
async getCategories() {
return this.get("/api/categories");
}
// Menu endpoints
async getMenu() {
return this.get("/api/menu");
}
// Homepage endpoints
async getHomepageSettings() {
return this.get("/api/homepage-settings");
}
}
// Create global instance
window.API = window.API || new APIClient();
// Helper function for loading states
window.withLoading = async function (element, asyncFn) {
if (!element) return asyncFn();
element.classList.add("loading");
element.setAttribute("aria-busy", "true");
try {
return await asyncFn();
} finally {
element.classList.remove("loading");
element.setAttribute("aria-busy", "false");
}
};
})();

View File

@@ -0,0 +1,62 @@
/**
* Back Button Navigation Control - SIMPLIFIED & FIXED
*
* Problem: History manipulation (replaceState/pushState) changes URL without reloading page
* Solution: Let browser handle navigation naturally, only intercept when necessary
*
* Requirements:
* 1. Natural browser back/forward navigation (URL changes = page loads)
* 2. Prevent going back past home page
* 3. Ensure page is always interactive after navigation
*/
(function () {
"use strict";
// Configuration
const HOME_PAGES = ["/", "/home.html", "/index.html"];
const HOME_URL = "/home.html";
/**
* Handle popstate (back/forward button) events
* This fires AFTER the browser has already navigated (URL changed)
*/
function handlePopState(event) {
// Get the NEW current path (browser already changed it)
const currentPath = window.location.pathname;
// Ensure page is always interactive after back/forward
document.body.classList.remove("page-transitioning");
document.body.style.opacity = "1";
sessionStorage.removeItem("page-transitioning");
// If we're on home page after a back navigation
// prevent going back further by adding home to history
if (HOME_PAGES.includes(currentPath)) {
// Use setTimeout to avoid interfering with current popstate
setTimeout(() => {
window.history.pushState({ page: "home" }, "", HOME_URL);
}, 0);
}
}
/**
* Prevent going back past home page
* Add an extra entry so back button stays on home
*/
function preventBackPastHome() {
const currentPath = window.location.pathname;
if (HOME_PAGES.includes(currentPath)) {
// Add an extra home entry
window.history.pushState({ page: "home", initial: true }, "", HOME_URL);
}
}
// Initialize: Add home history entry if on home page
preventBackPastHome();
// Listen for popstate (back/forward button)
// NOTE: Browser handles the actual navigation (page reload)
// We just ensure interactivity and prevent going back past home
window.addEventListener("popstate", handlePopState);
})();

View File

@@ -0,0 +1,155 @@
/**
* Shared Cart and Wishlist Functions
* Simple localStorage-based implementation that works on all pages
*/
(function () {
"use strict";
// Cart Functions
window.addToCart = function (productId, name, price, imageurl) {
try {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const existingItem = cart.find((item) => item.id === productId);
if (existingItem) {
existingItem.quantity = (existingItem.quantity || 1) + 1;
} else {
cart.push({
id: productId,
name,
price: parseFloat(price),
imageurl,
quantity: 1,
});
}
localStorage.setItem("cart", JSON.stringify(cart));
updateCartBadge();
showNotification(`${name} added to cart!`, "success");
} catch (e) {
console.error("Cart error:", e);
showNotification("Added to cart!", "success");
}
};
// Wishlist Functions
window.addToWishlist = function (productId, name, price, imageurl) {
try {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
const exists = wishlist.find((item) => item.id === productId);
if (!exists) {
wishlist.push({
id: productId,
name,
price: parseFloat(price),
imageurl,
});
localStorage.setItem("wishlist", JSON.stringify(wishlist));
updateWishlistBadge();
showNotification(`${name} added to wishlist!`, "success");
} else {
showNotification("Already in wishlist!", "info");
}
} catch (e) {
console.error("Wishlist error:", e);
showNotification("Added to wishlist!", "success");
}
};
// Update Badge Functions
function updateCartBadge() {
try {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
const badge = document.querySelector(".cart-badge");
if (badge) {
const total = cart.reduce((sum, item) => sum + (item.quantity || 1), 0);
badge.textContent = total;
badge.style.display = total > 0 ? "flex" : "none";
}
} catch (e) {
console.error("Badge update error:", e);
}
}
function updateWishlistBadge() {
try {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
const badge = document.querySelector(".wishlist-badge");
if (badge) {
badge.textContent = wishlist.length;
badge.style.display = wishlist.length > 0 ? "flex" : "none";
}
} catch (e) {
console.error("Badge update error:", e);
}
}
// Notification Function
function showNotification(message, type = "info") {
// Remove existing notifications
document.querySelectorAll(".cart-notification").forEach((n) => n.remove());
const notification = document.createElement("div");
notification.className = `cart-notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: ${
type === "success"
? "#10b981"
: type === "error"
? "#ef4444"
: "#3b82f6"
};
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideInFromRight 0.3s ease;
`;
// Add animation styles if not already present
if (!document.getElementById("notification-animations")) {
const style = document.createElement("style");
style.id = "notification-animations";
style.textContent = `
@keyframes slideInFromRight {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutToRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOutToRight 0.3s ease";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Initialize badges on page load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
updateCartBadge();
updateWishlistBadge();
});
} else {
updateCartBadge();
updateWishlistBadge();
}
// Expose update functions globally
window.updateCartBadge = updateCartBadge;
window.updateWishlistBadge = updateWishlistBadge;
})();

View File

@@ -0,0 +1,319 @@
/**
* Shopping Cart Component
* Handles cart dropdown, updates, and interactions
*/
(function () {
"use strict";
class ShoppingCart {
constructor() {
this.cartToggle = document.getElementById("cartToggle");
this.cartPanel = document.getElementById("cartPanel");
this.cartContent = document.getElementById("cartContent");
this.cartClose = document.getElementById("cartClose");
this.isOpen = false;
this.init();
}
init() {
this.setupEventListeners();
this.render();
}
setupEventListeners() {
if (this.cartToggle) {
this.cartToggle.addEventListener("click", () => this.toggle());
}
if (this.cartClose) {
this.cartClose.addEventListener("click", () => this.close());
}
// Close when clicking outside
document.addEventListener("click", (e) => {
if (this.isOpen && !e.target.closest(".cart-dropdown-wrapper")) {
this.close();
}
});
// Listen for cart updates
window.addEventListener("cart-updated", () => this.render());
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.cartPanel) {
this.cartPanel.classList.add("active");
this.cartPanel.setAttribute("aria-hidden", "false");
this.isOpen = true;
this.render();
}
}
close() {
if (this.cartPanel) {
this.cartPanel.classList.remove("active");
this.cartPanel.setAttribute("aria-hidden", "true");
this.isOpen = false;
}
}
render() {
if (!this.cartContent) return;
const cart = window.AppState.cart;
if (cart.length === 0) {
this.cartContent.innerHTML =
'<p class="empty-state">Your cart is empty</p>';
this.updateFooter(null);
return;
}
const html = cart.map((item) => this.renderCartItem(item)).join("");
this.cartContent.innerHTML = html;
// Add event listeners to cart items
this.setupCartItemListeners();
// Update footer with total
this.updateFooter(window.AppState.getCartTotal());
}
renderCartItem(item) {
const imageUrl =
item.imageUrl || item.image_url || "/assets/images/placeholder.jpg";
const title = window.Utils.escapeHtml(
item.title || item.name || "Product"
);
const price = window.Utils.formatCurrency(item.price || 0);
const subtotal = window.Utils.formatCurrency(
(item.price || 0) * item.quantity
);
return `
<div class="cart-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${title}" class="cart-item-image" loading="lazy">
<div class="cart-item-details">
<h4 class="cart-item-title">${title}</h4>
<p class="cart-item-price">${price}</p>
<div class="cart-item-quantity">
<button class="quantity-btn quantity-minus" data-id="${item.id}" aria-label="Decrease quantity">
<i class="bi bi-dash"></i>
</button>
<span class="quantity-value">${item.quantity}</span>
<button class="quantity-btn quantity-plus" data-id="${item.id}" aria-label="Increase quantity">
<i class="bi bi-plus"></i>
</button>
</div>
<p class="cart-item-subtotal">${subtotal}</p>
</div>
<button class="cart-item-remove" data-id="${item.id}" aria-label="Remove from cart">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
setupCartItemListeners() {
// Remove buttons
this.cartContent.querySelectorAll(".cart-item-remove").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
window.AppState.removeFromCart(id);
this.render();
});
});
// Quantity buttons
this.cartContent.querySelectorAll(".quantity-minus").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.cart.find((item) => item.id === id);
if (item && item.quantity > 1) {
window.AppState.updateCartQuantity(id, item.quantity - 1);
this.render();
}
});
});
this.cartContent.querySelectorAll(".quantity-plus").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.cart.find((item) => item.id === id);
if (item) {
window.AppState.updateCartQuantity(id, item.quantity + 1);
this.render();
}
});
});
}
updateFooter(total) {
const footer = this.cartPanel?.querySelector(".dropdown-foot");
if (!footer) return;
if (total === null) {
footer.innerHTML =
'<a href="/shop" class="btn-outline">Continue Shopping</a>';
} else {
footer.innerHTML = `
<div class="cart-total">
<span>Total:</span>
<strong>${window.Utils.formatCurrency(total)}</strong>
</div>
<a href="/shop" class="btn-text">Continue Shopping</a>
<button class="btn-primary-full" onclick="alert('Checkout coming soon!')">
Proceed to Checkout
</button>
`;
}
}
}
// Wishlist Component
class Wishlist {
constructor() {
this.wishlistToggle = document.getElementById("wishlistToggle");
this.wishlistPanel = document.getElementById("wishlistPanel");
this.wishlistContent = document.getElementById("wishlistContent");
this.wishlistClose = document.getElementById("wishlistClose");
this.isOpen = false;
this.init();
}
init() {
this.setupEventListeners();
this.render();
}
setupEventListeners() {
if (this.wishlistToggle) {
this.wishlistToggle.addEventListener("click", () => this.toggle());
}
if (this.wishlistClose) {
this.wishlistClose.addEventListener("click", () => this.close());
}
// Close when clicking outside
document.addEventListener("click", (e) => {
if (this.isOpen && !e.target.closest(".wishlist-dropdown-wrapper")) {
this.close();
}
});
// Listen for wishlist updates
window.addEventListener("wishlist-updated", () => this.render());
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.wishlistPanel) {
this.wishlistPanel.classList.add("active");
this.wishlistPanel.setAttribute("aria-hidden", "false");
this.isOpen = true;
this.render();
}
}
close() {
if (this.wishlistPanel) {
this.wishlistPanel.classList.remove("active");
this.wishlistPanel.setAttribute("aria-hidden", "true");
this.isOpen = false;
}
}
render() {
if (!this.wishlistContent) return;
const wishlist = window.AppState.wishlist;
if (wishlist.length === 0) {
this.wishlistContent.innerHTML =
'<p class="empty-state">Your wishlist is empty</p>';
return;
}
const html = wishlist
.map((item) => this.renderWishlistItem(item))
.join("");
this.wishlistContent.innerHTML = html;
// Add event listeners
this.setupWishlistItemListeners();
}
renderWishlistItem(item) {
const imageUrl =
item.imageUrl || item.image_url || "/assets/images/placeholder.jpg";
const title = window.Utils.escapeHtml(
item.title || item.name || "Product"
);
const price = window.Utils.formatCurrency(item.price || 0);
return `
<div class="wishlist-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${title}" class="wishlist-item-image" loading="lazy">
<div class="wishlist-item-details">
<h4 class="wishlist-item-title">${title}</h4>
<p class="wishlist-item-price">${price}</p>
<button class="btn-add-to-cart" data-id="${item.id}">Add to Cart</button>
</div>
<button class="wishlist-item-remove" data-id="${item.id}" aria-label="Remove from wishlist">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
setupWishlistItemListeners() {
// Remove buttons
this.wishlistContent
.querySelectorAll(".wishlist-item-remove")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
window.AppState.removeFromWishlist(id);
this.render();
});
});
// Add to cart buttons
this.wishlistContent
.querySelectorAll(".btn-add-to-cart")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = parseInt(e.currentTarget.dataset.id);
const item = window.AppState.wishlist.find(
(item) => item.id === id
);
if (item) {
window.AppState.addToCart(item);
}
});
});
}
}
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
new ShoppingCart();
new Wishlist();
});
} else {
new ShoppingCart();
new Wishlist();
}
})();

View File

@@ -0,0 +1,72 @@
/**
* Lazy Loading Images Script
* Optimizes image loading for better performance
*/
(function () {
"use strict";
// Check for Intersection Observer support
if (!("IntersectionObserver" in window)) {
// Fallback: load all images immediately
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
if (img.dataset.src) {
img.src = img.dataset.src;
}
});
return;
}
// Configure intersection observer
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
// Load the image
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
// Optional: load srcset
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute("data-srcset");
}
// Add loaded class for fade-in effect
img.classList.add("loaded");
// Stop observing this image
observer.unobserve(img);
}
});
},
{
// Start loading when image is 50px from viewport
rootMargin: "50px 0px",
threshold: 0.01,
}
);
// Observe all lazy images
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
lazyImages.forEach((img) => imageObserver.observe(img));
// Add CSS for fade-in effect if not already present
if (!document.getElementById("lazy-load-styles")) {
const style = document.createElement("style");
style.id = "lazy-load-styles";
style.textContent = `
img[loading="lazy"] {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
img[loading="lazy"].loaded {
opacity: 1;
}
`;
document.head.appendChild(style);
}
})();

View File

@@ -0,0 +1,350 @@
/**
* Main Application JavaScript
* Handles global state management, API integration, and core functionality
*/
(function () {
"use strict";
// Global state management
window.AppState = {
cart: [],
wishlist: [],
products: [],
settings: null,
user: null,
// Initialize state from localStorage
init() {
this.loadCart();
this.loadWishlist();
this.updateUI();
},
// Cart management
loadCart() {
try {
const saved = localStorage.getItem("cart");
this.cart = saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error loading cart:", error);
this.cart = [];
}
},
saveCart() {
try {
localStorage.setItem("cart", JSON.stringify(this.cart));
this.updateUI();
} catch (error) {
console.error("Error saving cart:", error);
}
},
addToCart(product, quantity = 1) {
const existing = this.cart.find((item) => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.cart.push({ ...product, quantity });
}
this.saveCart();
this.showNotification("Added to cart", "success");
},
removeFromCart(productId) {
this.cart = this.cart.filter((item) => item.id !== productId);
this.saveCart();
this.showNotification("Removed from cart", "info");
},
updateCartQuantity(productId, quantity) {
const item = this.cart.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
this.saveCart();
}
},
getCartTotal() {
return this.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
getCartCount() {
return this.cart.reduce((sum, item) => sum + item.quantity, 0);
},
// Wishlist management
loadWishlist() {
try {
const saved = localStorage.getItem("wishlist");
this.wishlist = saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error loading wishlist:", error);
this.wishlist = [];
}
},
saveWishlist() {
try {
localStorage.setItem("wishlist", JSON.stringify(this.wishlist));
this.updateUI();
} catch (error) {
console.error("Error saving wishlist:", error);
}
},
addToWishlist(product) {
if (!this.wishlist.find((item) => item.id === product.id)) {
this.wishlist.push(product);
this.saveWishlist();
this.showNotification("Added to wishlist", "success");
}
},
removeFromWishlist(productId) {
this.wishlist = this.wishlist.filter((item) => item.id !== productId);
this.saveWishlist();
this.showNotification("Removed from wishlist", "info");
},
isInWishlist(productId) {
return this.wishlist.some((item) => item.id === productId);
},
// UI updates
updateUI() {
this.updateCartUI();
this.updateWishlistUI();
},
updateCartUI() {
const count = this.getCartCount();
const badge = document.getElementById("cartCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
updateWishlistUI() {
const count = this.wishlist.length;
const badge = document.getElementById("wishlistCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
// Notifications
showNotification(message, type = "info") {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.setAttribute("role", "alert");
notification.setAttribute("aria-live", "polite");
document.body.appendChild(notification);
setTimeout(() => notification.classList.add("show"), 10);
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 3000);
},
};
// API Client
window.API = {
baseURL: "/api",
async request(endpoint, options = {}) {
try {
const response = await fetch(this.baseURL + endpoint, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("API request failed:", error);
throw error;
}
},
// Product endpoints
async getProducts(filters = {}) {
const params = new URLSearchParams(filters);
return this.request(`/products?${params}`);
},
async getProduct(id) {
return this.request(`/products/${id}`);
},
async getFeaturedProducts() {
return this.request("/products/featured");
},
// Settings endpoint
async getSettings() {
return this.request("/settings");
},
// Homepage endpoint
async getHomepageSettings() {
return this.request("/homepage/settings");
},
// Menu endpoint
async getMenu() {
return this.request("/menu");
},
// Blog endpoints
async getBlogPosts() {
return this.request("/blog");
},
async getBlogPost(id) {
return this.request(`/blog/${id}`);
},
// Portfolio endpoints
async getPortfolioProjects() {
return this.request("/portfolio");
},
async getPortfolioProject(id) {
return this.request(`/portfolio/${id}`);
},
// Pages endpoints
async getPages() {
return this.request("/pages");
},
async getPage(slug) {
return this.request(`/pages/${slug}`);
},
};
// Utility functions
window.Utils = {
// Format currency
formatCurrency(amount) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
},
// Format date
formatDate(date) {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
},
// Debounce function
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// Get URL parameter
getUrlParameter(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
},
// Safe HTML encode
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
// Show loading state
showLoading(element) {
if (element) {
element.classList.add("loading");
element.setAttribute("aria-busy", "true");
}
},
hideLoading(element) {
if (element) {
element.classList.remove("loading");
element.setAttribute("aria-busy", "false");
}
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.AppState.init();
});
} else {
window.AppState.init();
}
// Add notification styles if not exists
if (!document.getElementById("notification-styles")) {
const style = document.createElement("style");
style.id = "notification-styles";
style.textContent = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
opacity: 0;
transform: translateX(400px);
transition: all 0.3s ease;
max-width: 300px;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification-success {
border-left: 4px solid #28a745;
}
.notification-error {
border-left: 4px solid #dc3545;
}
.notification-info {
border-left: 4px solid #17a2b8;
}
.notification-warning {
border-left: 4px solid #ffc107;
}
`;
document.head.appendChild(style);
}
})();

View File

@@ -0,0 +1,203 @@
/**
* Navigation Component
* Handles mobile menu, dropdowns, and accessibility
*/
(function () {
"use strict";
class Navigation {
constructor() {
this.mobileMenuToggle = document.getElementById("mobileMenuToggle");
this.mobileMenu = document.getElementById("mobileMenu");
this.mobileMenuClose = document.getElementById("mobileMenuClose");
this.overlay = document.getElementById("mobileMenuOverlay");
this.body = document.body;
this.init();
}
init() {
this.setupMobileMenu();
this.setupAccessibility();
this.highlightCurrentPage();
this.setupKeyboardNavigation();
}
setupMobileMenu() {
// Open mobile menu
if (this.mobileMenuToggle) {
this.mobileMenuToggle.addEventListener("click", () =>
this.openMobileMenu()
);
}
// Close mobile menu
if (this.mobileMenuClose) {
this.mobileMenuClose.addEventListener("click", () =>
this.closeMobileMenu()
);
}
if (this.overlay) {
this.overlay.addEventListener("click", () => this.closeMobileMenu());
}
// Close on ESC key
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
this.mobileMenu &&
this.mobileMenu.classList.contains("active")
) {
this.closeMobileMenu();
}
});
}
openMobileMenu() {
if (this.mobileMenu) {
this.mobileMenu.classList.add("active");
this.mobileMenu.setAttribute("aria-hidden", "false");
this.body.style.overflow = "hidden";
if (this.overlay) {
this.overlay.classList.add("active");
}
// Focus first link
const firstLink = this.mobileMenu.querySelector("a");
if (firstLink) {
setTimeout(() => firstLink.focus(), 100);
}
}
}
closeMobileMenu() {
if (this.mobileMenu) {
this.mobileMenu.classList.remove("active");
this.mobileMenu.setAttribute("aria-hidden", "true");
this.body.style.overflow = "";
if (this.overlay) {
this.overlay.classList.remove("active");
}
// Return focus to toggle button
if (this.mobileMenuToggle) {
this.mobileMenuToggle.focus();
}
}
}
setupAccessibility() {
// Wait for body to exist
if (!document.body) return;
// Add ARIA labels to nav items
const navLinks = document.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
if (!link.getAttribute("aria-label")) {
link.setAttribute(
"aria-label",
`Navigate to ${link.textContent.trim()}`
);
}
});
// Add skip to main content link
if (!document.getElementById("skip-to-main")) {
const skipLink = document.createElement("a");
skipLink.id = "skip-to-main";
skipLink.href = "#main-content";
skipLink.textContent = "Skip to main content";
skipLink.className = "skip-link";
document.body.insertBefore(skipLink, document.body.firstChild);
// Add styles for skip link
if (!document.getElementById("skip-link-styles")) {
const style = document.createElement("style");
style.id = "skip-link-styles";
style.textContent = `
.skip-link {
position: fixed;
top: -100px;
left: 0;
padding: 10px 20px;
background: #000;
color: #fff;
z-index: 10001;
text-decoration: none;
border-radius: 0 0 8px 0;
}
.skip-link:focus {
top: 0;
}
`;
document.head.appendChild(style);
}
}
}
highlightCurrentPage() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll(".nav-link, .mobile-link");
navLinks.forEach((link) => {
const href = link.getAttribute("href");
if (
href &&
(currentPath === href || currentPath.startsWith(href + "/"))
) {
link.classList.add("active");
link.setAttribute("aria-current", "page");
} else {
link.classList.remove("active");
link.removeAttribute("aria-current");
}
});
}
setupKeyboardNavigation() {
// Tab trap in mobile menu when open
if (this.mobileMenu) {
const focusableElements = this.mobileMenu.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
this.mobileMenu.addEventListener("keydown", (e) => {
if (
e.key === "Tab" &&
this.mobileMenu.classList.contains("active")
) {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
});
}
}
}
}
// Initialize navigation when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
new Navigation();
});
} else {
new Navigation();
}
})();

View File

@@ -0,0 +1,224 @@
/**
* Notification System
* Accessible toast notifications
*/
(function () {
"use strict";
class NotificationManager {
constructor() {
this.container = null;
this.notifications = new Map();
this.init();
}
init() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () =>
this.createContainer()
);
} else {
this.createContainer();
}
}
createContainer() {
if (!document.body || this.container) return;
this.container = document.createElement("div");
this.container.id = "notification-container";
this.container.setAttribute("aria-live", "polite");
this.container.setAttribute("aria-atomic", "true");
this.container.className = "notification-container";
const style = document.createElement("style");
style.textContent = `
.notification-container {
position: fixed;
top: 80px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
pointer-events: none;
}
.notification {
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 12px;
color: white;
font-size: 14px;
font-weight: 500;
pointer-events: auto;
animation: slideInRight 0.3s ease;
min-width: 250px;
}
.notification.removing {
animation: slideOutRight 0.3s ease;
}
.notification-success {
background: #10b981;
}
.notification-error {
background: #ef4444;
}
.notification-info {
background: #3b82f6;
}
.notification-warning {
background: #f59e0b;
}
.notification-icon {
font-size: 18px;
flex-shrink: 0;
}
.notification-message {
flex: 1;
}
.notification-close {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 4px;
opacity: 0.8;
transition: opacity 0.2s;
}
.notification-close:hover {
opacity: 1;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
@media (max-width: 640px) {
.notification-container {
right: 10px;
left: 10px;
max-width: none;
}
.notification {
min-width: auto;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(this.container);
}
show(message, type = "info", duration = 3000) {
if (!this.container) this.createContainer();
if (!this.container) return;
const id = Date.now() + Math.random();
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.setAttribute("role", "alert");
const icons = {
success: "✓",
error: "✕",
info: "",
warning: "⚠",
};
notification.innerHTML = `
<span class="notification-icon">${icons[type] || icons.info}</span>
<span class="notification-message">${this.escapeHtml(message)}</span>
<button class="notification-close" aria-label="Close notification">×</button>
`;
const closeBtn = notification.querySelector(".notification-close");
closeBtn.addEventListener("click", () => this.remove(id));
this.container.appendChild(notification);
this.notifications.set(id, notification);
if (duration > 0) {
setTimeout(() => this.remove(id), duration);
}
return id;
}
remove(id) {
const notification = this.notifications.get(id);
if (!notification) return;
notification.classList.add("removing");
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
this.notifications.delete(id);
}, 300);
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
success(message, duration) {
return this.show(message, "success", duration);
}
error(message, duration) {
return this.show(message, "error", duration);
}
info(message, duration) {
return this.show(message, "info", duration);
}
warning(message, duration) {
return this.show(message, "warning", duration);
}
}
// Create global instance
window.Notifications = window.Notifications || new NotificationManager();
// Legacy compatibility
window.showNotification = function (message, type = "info") {
window.Notifications.show(message, type);
};
})();

View File

@@ -0,0 +1,555 @@
/**
* Page Transitions and Smooth Navigation
* Handles page loading, transitions, and history management
*/
class PageTransitions {
constructor() {
this.transitionDuration = 300;
this.isTransitioning = false;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Add transition wrapper if it doesn't exist
if (!document.getElementById("page-transition")) {
const wrapper = document.createElement("div");
wrapper.id = "page-transition";
wrapper.className = "page-transition";
// Wrap main content
const main = document.querySelector("main") || document.body;
const parent = main.parentNode;
parent.insertBefore(wrapper, main);
wrapper.appendChild(main);
}
// Add fade-in on page load
this.fadeIn();
// Intercept navigation clicks
this.setupLinkInterception();
// Handle back/forward buttons
window.addEventListener("popstate", (e) => {
if (e.state && e.state.url) {
this.navigate(e.state.url, false);
}
});
// Add scroll restoration
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
}
}
fadeIn() {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-in");
setTimeout(() => {
wrapper.classList.remove("fade-in");
}, this.transitionDuration);
}
}
fadeOut(callback) {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-out");
setTimeout(() => {
if (callback) callback();
wrapper.classList.remove("fade-out");
}, this.transitionDuration);
} else {
if (callback) callback();
}
}
setupLinkInterception() {
document.addEventListener("click", (e) => {
const link = e.target.closest("a");
// Check if it's a valid internal link
if (!link) return;
if (link.hasAttribute("data-no-transition")) return;
if (link.target === "_blank") return;
if (link.hasAttribute("download")) return;
const href = link.getAttribute("href");
if (
!href ||
href.startsWith("#") ||
href.startsWith("mailto:") ||
href.startsWith("tel:")
)
return;
// Check if it's an external link
const url = new URL(href, window.location.origin);
if (url.origin !== window.location.origin) return;
// Intercept the navigation
e.preventDefault();
this.navigate(href, true);
});
}
navigate(url, updateHistory = true) {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.fadeOut(() => {
if (updateHistory) {
history.pushState({ url }, "", url);
}
window.location.href = url;
});
}
// Scroll to element with smooth animation
scrollTo(selector, offset = 0) {
const element = document.querySelector(selector);
if (!element) return;
const top =
element.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: "smooth",
});
}
// Scroll to top
scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
}
/**
* Lazy Loading Images
* Improves performance by loading images only when they're visible
*/
class LazyLoader {
constructor() {
this.images = [];
this.observer = null;
this.init();
}
init() {
// Find all lazy images
this.images = document.querySelectorAll(
'img[data-src], img[loading="lazy"]'
);
// Set up Intersection Observer
if ("IntersectionObserver" in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
}
});
},
{
rootMargin: "50px",
}
);
this.images.forEach((img) => this.observer.observe(img));
} else {
// Fallback for older browsers
this.images.forEach((img) => this.loadImage(img));
}
}
loadImage(img) {
const src = img.getAttribute("data-src");
if (src) {
img.src = src;
img.removeAttribute("data-src");
}
// Add fade-in effect
img.addEventListener("load", () => {
img.classList.add("loaded");
});
if (this.observer) {
this.observer.unobserve(img);
}
}
// Add new images to observer
observe(images) {
if (!images) return;
const imageList = Array.isArray(images) ? images : [images];
imageList.forEach((img) => {
if (this.observer) {
this.observer.observe(img);
} else {
this.loadImage(img);
}
});
}
}
/**
* Smooth Scroll Handler
* Adds smooth scrolling to anchor links
*/
class SmoothScroll {
constructor() {
this.init();
}
init() {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
const href = anchor.getAttribute("href");
if (href === "#") return;
e.preventDefault();
const target = document.querySelector(href);
if (target) {
const offset = 80; // Account for fixed header
const top =
target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: "smooth",
});
// Update URL without scrolling
history.pushState(null, "", href);
}
});
});
}
}
/**
* Back to Top Button
* Shows/hides button based on scroll position
*/
class BackToTop {
constructor() {
this.button = null;
this.scrollThreshold = 300;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create button if it doesn't exist
this.button = document.getElementById("back-to-top");
if (!this.button) {
this.button = document.createElement("button");
this.button.id = "back-to-top";
this.button.className = "back-to-top";
this.button.innerHTML = "↑";
this.button.setAttribute("aria-label", "Back to top");
document.body.appendChild(this.button);
}
// Handle scroll
window.addEventListener("scroll", () => {
if (window.pageYOffset > this.scrollThreshold) {
this.button.classList.add("visible");
} else {
this.button.classList.remove("visible");
}
});
// Handle click
this.button.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
}
/**
* Loading Overlay
* Shows loading state during async operations
*/
class LoadingOverlay {
constructor() {
this.overlay = null;
this.activeOperations = 0;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create overlay if it doesn't exist
this.overlay = document.getElementById("loading-overlay");
if (!this.overlay) {
this.overlay = document.createElement("div");
this.overlay.id = "loading-overlay";
this.overlay.className = "loading-overlay";
this.overlay.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<p>Loading...</p>
</div>
`;
document.body.appendChild(this.overlay);
}
}
show() {
this.activeOperations++;
this.overlay.classList.add("active");
document.body.style.overflow = "hidden";
}
hide() {
this.activeOperations = Math.max(0, this.activeOperations - 1);
if (this.activeOperations === 0) {
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
// Force hide regardless of operation count
forceHide() {
this.activeOperations = 0;
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
/**
* Page Visibility Handler
* Handles actions when page becomes visible/hidden
*/
class PageVisibility {
constructor() {
this.callbacks = {
visible: [],
hidden: [],
};
this.init();
}
init() {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.callbacks.hidden.forEach((cb) => cb());
} else {
this.callbacks.visible.forEach((cb) => cb());
}
});
}
onVisible(callback) {
this.callbacks.visible.push(callback);
}
onHidden(callback) {
this.callbacks.hidden.push(callback);
}
}
/**
* Network Status Handler
* Monitors online/offline status
*/
class NetworkStatus {
constructor() {
this.isOnline = navigator.onLine;
this.callbacks = {
online: [],
offline: [],
};
this.init();
}
init() {
window.addEventListener("online", () => {
this.isOnline = true;
this.callbacks.online.forEach((cb) => cb());
this.showNotification("Back online", "success");
});
window.addEventListener("offline", () => {
this.isOnline = false;
this.callbacks.offline.forEach((cb) => cb());
this.showNotification("No internet connection", "error");
});
}
onOnline(callback) {
this.callbacks.online.push(callback);
}
onOffline(callback) {
this.callbacks.offline.push(callback);
}
showNotification(message, type) {
if (window.Utils && window.Utils.notify) {
window.Utils.notify(message, type);
}
}
}
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPageTransitions);
} else {
initPageTransitions();
}
function initPageTransitions() {
// Initialize all modules
window.pageTransitions = new PageTransitions();
window.lazyLoader = new LazyLoader();
window.smoothScroll = new SmoothScroll();
window.backToTop = new BackToTop();
window.loadingOverlay = new LoadingOverlay();
window.pageVisibility = new PageVisibility();
window.networkStatus = new NetworkStatus();
console.log("Page transitions initialized");
}
// Add CSS if not already present
if (!document.getElementById("page-transitions-styles")) {
const style = document.createElement("style");
style.id = "page-transitions-styles";
style.textContent = `
.page-transition {
opacity: 1;
transition: opacity 300ms ease;
}
.page-transition.fade-in {
opacity: 0;
animation: fadeIn 300ms ease forwards;
}
.page-transition.fade-out {
opacity: 1;
animation: fadeOut 300ms ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
img[data-src] {
opacity: 0;
transition: opacity 300ms ease;
}
img.loaded {
opacity: 1;
}
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #667eea;
color: white;
border: none;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 999;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background: #5568d3;
transform: translateY(-2px);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.95);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9999;
}
.loading-overlay.active {
opacity: 1;
visibility: visible;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner p {
color: #667eea;
font-size: 16px;
font-weight: 600;
margin: 0;
}
`;
document.head.appendChild(style);
}

View File

@@ -0,0 +1,306 @@
/**
* Shopping/Products Component
* Handles product display, filtering, and interactions
*/
(function () {
"use strict";
class ShoppingPage {
constructor() {
this.productsContainer = document.getElementById("productsContainer");
this.loadingIndicator = document.getElementById("loadingIndicator");
this.errorContainer = document.getElementById("errorContainer");
this.currentCategory = window.Utils.getUrlParameter("category") || "all";
this.currentSort = "newest";
this.products = [];
this.init();
}
async init() {
this.setupEventListeners();
await this.loadProducts();
}
setupEventListeners() {
// Category filters
document.querySelectorAll("[data-category]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
this.currentCategory = e.currentTarget.dataset.category;
this.filterProducts();
});
});
// Sort dropdown
const sortSelect = document.getElementById("sortSelect");
if (sortSelect) {
sortSelect.addEventListener("change", (e) => {
this.currentSort = e.target.value;
this.filterProducts();
});
}
// Search
const searchInput = document.getElementById("productSearch");
if (searchInput) {
searchInput.addEventListener(
"input",
window.Utils.debounce((e) => {
this.searchProducts(e.target.value);
}, 300)
);
}
}
async loadProducts() {
if (!this.productsContainer) return;
try {
this.showLoading();
const response = await window.API.getProducts();
this.products = response.products || response.data || [];
this.renderProducts(this.products);
this.hideLoading();
} catch (error) {
console.error("Error loading products:", error);
this.showError("Failed to load products. Please try again later.");
this.hideLoading();
}
}
filterProducts() {
let filtered = [...this.products];
// Filter by category
if (this.currentCategory && this.currentCategory !== "all") {
filtered = filtered.filter(
(p) =>
p.category?.toLowerCase() === this.currentCategory.toLowerCase()
);
}
// Sort products
filtered = this.sortProducts(filtered);
this.renderProducts(filtered);
}
sortProducts(products) {
switch (this.currentSort) {
case "price-low":
return products.sort((a, b) => (a.price || 0) - (b.price || 0));
case "price-high":
return products.sort((a, b) => (b.price || 0) - (a.price || 0));
case "name":
return products.sort((a, b) =>
(a.title || a.name || "").localeCompare(b.title || b.name || "")
);
case "newest":
default:
return products.sort(
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
);
}
}
searchProducts(query) {
if (!query.trim()) {
this.filterProducts();
return;
}
const searchTerm = query.toLowerCase();
const filtered = this.products.filter((p) => {
const title = (p.title || p.name || "").toLowerCase();
const description = (p.description || "").toLowerCase();
const category = (p.category || "").toLowerCase();
return (
title.includes(searchTerm) ||
description.includes(searchTerm) ||
category.includes(searchTerm)
);
});
this.renderProducts(filtered);
}
renderProducts(products) {
if (!this.productsContainer) return;
if (products.length === 0) {
this.productsContainer.innerHTML = `
<div class="no-products">
<i class="bi bi-inbox" style="font-size: 48px; opacity: 0.5;"></i>
<p>No products found</p>
</div>
`;
return;
}
const html = products
.map((product) => this.renderProductCard(product))
.join("");
this.productsContainer.innerHTML = html;
// Setup product card listeners
this.setupProductListeners();
}
renderProductCard(product) {
const id = product.id;
const title = window.Utils?.escapeHtml
? window.Utils.escapeHtml(product.title || product.name || "Product")
: product.title || product.name || "Product";
const price = window.Utils?.formatCurrency
? window.Utils.formatCurrency(product.price || 0)
: `$${parseFloat(product.price || 0).toFixed(2)}`;
// Get image URL from multiple possible sources
let imageUrl = "/assets/images/placeholder.jpg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
const primaryImg = product.images.find((img) => img.is_primary);
imageUrl = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageUrl) {
imageUrl = product.imageUrl;
} else if (product.image_url) {
imageUrl = product.image_url;
}
// Get description
const description =
product.shortdescription ||
(product.description
? product.description.substring(0, 100) + "..."
: "");
const isInWishlist = window.AppState?.isInWishlist(id) || false;
return `
<article class="product-card" data-id="${id}">
<div class="product-image-wrapper">
<img src="${imageUrl}" alt="${title}" class="product-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
<button
class="wishlist-btn ${isInWishlist ? "active" : ""}"
data-id="${id}"
aria-label="${
isInWishlist ? "Remove from wishlist" : "Add to wishlist"
}"
>
<i class="bi bi-heart${isInWishlist ? "-fill" : ""}"></i>
</button>
</div>
<div class="product-info">
<a href="/product?id=${id}" style="text-decoration: none; color: inherit;">
<h3 class="product-title">${title}</h3>
</a>
${
description
? `<div class="product-description">${description}</div>`
: ""
}
<p class="product-price">${price}</p>
<div class="product-actions">
<button class="btn-add-to-cart" data-id="${id}" style="flex: 1;">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</div>
</div>
</article>
`;
}
setupProductListeners() {
// Add to cart buttons
this.productsContainer
.querySelectorAll(".btn-add-to-cart")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
window.AppState.addToCart(product);
}
});
});
// Wishlist buttons
this.productsContainer
.querySelectorAll(".wishlist-btn")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
if (window.AppState.isInWishlist(id)) {
window.AppState.removeFromWishlist(id);
} else {
window.AppState.addToWishlist(product);
}
this.renderProducts(this.products);
}
});
});
}
showLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "flex";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "0.5";
}
}
hideLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "none";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "1";
}
}
showError(message) {
if (this.errorContainer) {
this.errorContainer.innerHTML = `
<div class="error-message" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<p>${window.Utils.escapeHtml(message)}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
this.errorContainer.style.display = "block";
}
}
}
// Initialize on shop/products pages
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
});
} else {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
}
})();

View File

@@ -0,0 +1,236 @@
/**
* Global State Management
* Centralized state for cart, wishlist, and user preferences
*/
(function () {
"use strict";
class StateManager {
constructor() {
this.state = {
cart: [],
wishlist: [],
user: null,
preferences: {},
};
this.listeners = {};
this.init();
}
init() {
this.loadFromStorage();
this.setupStorageSync();
}
loadFromStorage() {
try {
this.state.cart = JSON.parse(localStorage.getItem("cart") || "[]");
this.state.wishlist = JSON.parse(
localStorage.getItem("wishlist") || "[]"
);
this.state.preferences = JSON.parse(
localStorage.getItem("preferences") || "{}"
);
} catch (e) {
console.error("State load error:", e);
}
}
saveToStorage() {
try {
localStorage.setItem("cart", JSON.stringify(this.state.cart));
localStorage.setItem("wishlist", JSON.stringify(this.state.wishlist));
localStorage.setItem(
"preferences",
JSON.stringify(this.state.preferences)
);
} catch (e) {
console.error("State save error:", e);
}
}
setupStorageSync() {
window.addEventListener("storage", (e) => {
if (e.key === "cart" || e.key === "wishlist") {
this.loadFromStorage();
this.emit("stateChanged", { key: e.key });
}
});
}
// Cart methods
addToCart(product, quantity = 1) {
const existing = this.state.cart.find((item) => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.state.cart.push({
...product,
quantity,
addedAt: Date.now(),
});
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
return this.state.cart;
}
removeFromCart(productId) {
this.state.cart = this.state.cart.filter((item) => item.id !== productId);
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
return this.state.cart;
}
updateCartQuantity(productId, quantity) {
const item = this.state.cart.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
return this.removeFromCart(productId);
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
}
return this.state.cart;
}
getCart() {
return this.state.cart;
}
getCartTotal() {
return this.state.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
getCartCount() {
return this.state.cart.reduce((sum, item) => sum + item.quantity, 0);
}
clearCart() {
this.state.cart = [];
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
}
// Wishlist methods
addToWishlist(product) {
const exists = this.state.wishlist.find((item) => item.id === product.id);
if (!exists) {
this.state.wishlist.push({
...product,
addedAt: Date.now(),
});
this.saveToStorage();
this.emit("wishlistUpdated", this.state.wishlist);
return true;
}
return false;
}
removeFromWishlist(productId) {
this.state.wishlist = this.state.wishlist.filter(
(item) => item.id !== productId
);
this.saveToStorage();
this.emit("wishlistUpdated", this.state.wishlist);
return this.state.wishlist;
}
getWishlist() {
return this.state.wishlist;
}
isInWishlist(productId) {
return this.state.wishlist.some((item) => item.id === productId);
}
// Event system
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(
(cb) => cb !== callback
);
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => {
try {
callback(data);
} catch (e) {
console.error(`Error in ${event} listener:`, e);
}
});
}
}
}
// Create global instance
window.StateManager = window.StateManager || new StateManager();
// Expose helper functions for backward compatibility
window.addToCart = function (productId, name, price, imageurl) {
const product = { id: productId, name, price: parseFloat(price), imageurl };
window.StateManager.addToCart(product, 1);
if (window.showNotification) {
window.showNotification(`${name} added to cart!`, "success");
}
};
window.addToWishlist = function (productId, name, price, imageurl) {
const product = { id: productId, name, price: parseFloat(price), imageurl };
const added = window.StateManager.addToWishlist(product);
if (window.showNotification) {
window.showNotification(
added ? `${name} added to wishlist!` : "Already in wishlist!",
added ? "success" : "info"
);
}
};
// Update badges on state changes
window.StateManager.on("cartUpdated", () => {
const badge = document.querySelector(".cart-badge");
if (badge) {
const count = window.StateManager.getCartCount();
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
});
window.StateManager.on("wishlistUpdated", () => {
const badge = document.querySelector(".wishlist-badge");
if (badge) {
const count = window.StateManager.getWishlist().length;
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
});
// Initialize badges
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.StateManager.emit("cartUpdated");
window.StateManager.emit("wishlistUpdated");
});
} else {
window.StateManager.emit("cartUpdated");
window.StateManager.emit("wishlistUpdated");
}
})();

View File

@@ -12,44 +12,45 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1735692200" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link active">Blog</a>
<a href="/blog" class="nav-link active">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
@@ -75,7 +76,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -104,10 +105,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -122,18 +123,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -145,7 +146,7 @@
</div>
</section>
<section class="blog-section" style="padding: 60px 0; background: #f8f9fa">
<section class="blog-section" style="padding: 60px 0; background: #ffebeb">
<div class="container">
<div id="loadingMessage" style="text-align: center; padding: 40px">
<div class="spinner-border text-primary" role="status">
@@ -196,28 +197,28 @@
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
@@ -227,7 +228,8 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>

View File

@@ -12,9 +12,10 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1767233028" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/responsive.css" />
<style>
@media (max-width: 768px) {
#contactForm > div[style*="grid-template-columns"] {
@@ -22,41 +23,42 @@
}
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
<a href="/blog" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link active">Contact</a>
<a href="/contact" class="nav-link active">Contact</a>
</li>
</ul>
</div>
@@ -82,7 +84,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -111,10 +113,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -129,18 +131,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -149,20 +151,28 @@
<section
class="contact-hero"
style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 80px 0 60px;
color: white;
background: linear-gradient(135deg, #f6ccde 0%, #fcb1d8 100%);
padding: 40px 0 30px;
color: #202023;
text-align: center;
"
>
<div class="container">
<h1 style="font-size: 2.5rem; margin-bottom: 16px; font-weight: 700">
<h1
style="
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
color: #202023;
"
>
Get In Touch
</h1>
<p
style="
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
color: #202023;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
"
@@ -494,28 +504,28 @@
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
@@ -525,7 +535,8 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>

343
website/public/faq.html Normal file
View File

@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FAQ - Sky Art Shop</title>
<meta
name="description"
content="Frequently asked questions about our products and services"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<style>
.privacy-hero {
background: linear-gradient(135deg, #f6ccde 0%, #fcb1d8 100%);
padding: 40px 0 30px;
color: #202023;
text-align: center;
}
.privacy-hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
color: #202023;
}
.privacy-hero p {
font-size: 1.1rem;
color: #202023;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
.privacy-content {
padding: 60px 0;
background: #ffebeb;
}
.privacy-text {
max-width: 900px;
margin: 0 auto;
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2);
line-height: 1.8;
border: 1px solid #ffd0d0;
}
.privacy-text h2 {
color: #202023;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.privacy-text h3 {
color: #202023;
margin-top: 25px;
margin-bottom: 12px;
font-weight: 600;
}
.privacy-text p {
color: #202023;
opacity: 0.8;
margin-bottom: 15px;
}
.privacy-text ul {
margin-bottom: 20px;
padding-left: 30px;
}
.privacy-text li {
margin-bottom: 8px;
color: #202023;
opacity: 0.8;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<div class="action-item wishlist-dropdown-wrapper">
<button
class="action-btn"
id="wishlistToggle"
aria-label="Wishlist"
>
<i class="bi bi-heart"></i>
<span class="action-badge" id="wishlistCount">0</span>
</button>
<div class="action-dropdown wishlist-dropdown" id="wishlistPanel">
<div class="dropdown-head">
<h3>My Wishlist</h3>
<button class="dropdown-close" id="wishlistClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="wishlistContent">
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
<div class="action-item cart-dropdown-wrapper">
<button
class="action-btn"
id="cartToggle"
aria-label="Shopping Cart"
>
<i class="bi bi-cart3"></i>
<span class="action-badge" id="cartCount">0</span>
</button>
<div class="action-dropdown cart-dropdown" id="cartPanel">
<div class="dropdown-head">
<h3>Shopping Cart</h3>
<button class="dropdown-close" id="cartClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="cartContent">
<p class="empty-state">Your cart is empty</p>
</div>
<div class="dropdown-foot">
<div class="cart-summary">
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
<button class="mobile-toggle" id="mobileMenuToggle" aria-label="Menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
<section class="privacy-hero">
<div class="container">
<h1>Privacy Policy</h1>
<p>Your privacy is important to us</p>
</div>
</section>
<section class="privacy-content">
<div class="container">
<div class="privacy-text" id="privacyContent">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading privacy policy...</p>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h3 class="footer-title">Sky Art Shop</h3>
<p class="footer-text">
Your destination for unique art pieces and creative supplies.
</p>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
<a href="#" class="social-link"
><i class="bi bi-instagram"></i
></a>
<a href="#" class="social-link"><i class="bi bi-twitter"></i></a>
<a href="#" class="social-link"
><i class="bi bi-pinterest"></i
></a>
</div>
</div>
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Load privacy policy content from API
async function loadFaqContent() {
try {
const response = await fetch("/api/pages/faq");
const data = await response.json();
if (data.success && data.page) {
const contentDiv = document.getElementById("privacyContent");
contentDiv.innerHTML =
data.page.content || "<p>Content not available.</p>";
// Update meta tags if available
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
} else {
document.getElementById("privacyContent").innerHTML =
"<p>Unable to load content.</p>";
}
} catch (error) {
console.error("Error loading privacy content:", error);
document.getElementById("privacyContent").innerHTML =
"<p>Error loading content.</p>";
}
}
// Load content when page loads
document.addEventListener("DOMContentLoaded", loadFaqContent);
</script>
</body>
</html>

View File

@@ -18,44 +18,73 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1735692200" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/responsive-enhanced.css" />
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
<style>
/* Product Title Link - Make entire title clickable */
.product-title-link {
text-decoration: none !important;
color: #202023 !important;
display: block !important;
cursor: pointer !important;
transition: color 0.3s ease;
position: relative;
z-index: 10;
}
.product-title-link:hover {
color: #fcb1d8 !important;
}
.product-title-link h3 {
color: inherit;
transition: color 0.3s ease;
margin: 0;
pointer-events: none;
}
.product-title-link:hover h3 {
color: #fcb1d8 !important;
}
</style>
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
<a href="/blog" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
@@ -81,7 +110,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -110,10 +139,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -128,18 +157,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -158,9 +187,7 @@
bring your artistic vision to life.
</p>
</div>
<a href="/shop.html" class="btn btn-primary" id="heroCtaBtn"
>Shop Now</a
>
<a href="/shop" class="btn btn-primary" id="heroCtaBtn">Shop Now</a>
</div>
<div class="hero-image" id="heroImageContainer">
<img
@@ -199,7 +226,7 @@
/>
</div>
</div>
<a href="/portfolio.html" class="btn btn-secondary">View Portfolio</a>
<a href="/portfolio" class="btn btn-secondary">View Portfolio</a>
</div>
</section>
@@ -214,7 +241,7 @@
<div class="product-card">
<div class="product-image">
<img
src="/assets/images/placeholder.jpg"
src="/assets/images/placeholder.svg"
alt="Product"
loading="lazy"
/>
@@ -222,7 +249,9 @@
<h3>Loading products...</h3>
</div>
</div>
<a href="/shop.html" class="btn btn-secondary">View All Products</a>
<div style="margin-top: 40px">
<a href="/shop" class="btn btn-secondary">View All Products</a>
</div>
</div>
</section>
@@ -249,28 +278,28 @@
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
@@ -280,14 +309,15 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/cart.js"></script>
<script>
// Load homepage settings
async function loadHomepageSettings() {
try {
const response = await fetch("/api/public/homepage/settings");
const response = await fetch("/api/homepage/settings");
if (response.ok) {
const data = await response.json();
if (data.success && data.settings) {
@@ -454,44 +484,78 @@
if (data.products && data.products.length > 0) {
const container = document.getElementById("featuredProducts");
container.innerHTML = data.products
.map(
(product) => `
.map((product) => {
// Get product image (primary or first from images array)
let productImage = "/assets/images/placeholder.svg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
const primaryImg = product.images.find(
(img) => img.is_primary
);
productImage = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageurl) {
productImage = product.imageurl;
}
return `
<div class="product-card">
<a href="/product.html?id=${
<a href="/product?id=${
product.id
}" class="product-link">
<div class="product-image">
<img src="${
product.imageurl ||
"/assets/images/placeholder.jpg"
}" alt="${
<img src="${productImage}" alt="${
product.name
}" loading="lazy" />
}" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'" />
</div>
<h3>${product.name}</h3>
</a>
<div class="product-info">
<a href="/product?id=${
product.id
}" class="product-title-link">
<h3>${product.name}</h3>
</a>
${
product.shortdescription ||
product.description
? `<div class="product-description">${
product.shortdescription ||
(product.description
? product.description.substring(
0,
100
) + "..."
: "")
}</div>`
: ""
}
<p class="price">$${parseFloat(
product.price
).toFixed(2)}</p>
</a>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
</div>
<div class="product-actions">
<button class="btn btn-small btn-icon" onclick="addToWishlist('${
product.id
}', '${product.name}', ${product.price}, '${
product.imageurl || ""
}')" aria-label="Add to wishlist">
}', '${product.name}', ${
product.price
}, '${productImage}')" aria-label="Add to wishlist">
<i class="bi bi-heart"></i>
</button>
<button class="btn btn-small btn-icon" onclick="addToCart('${
product.id
}', '${product.name}', ${product.price}, '${
product.imageurl || ""
}')" aria-label="Add to cart">
}', '${product.name}', ${
product.price
}, '${productImage}')" aria-label="Add to cart">
<i class="bi bi-cart-plus"></i>
</button>
</div>
</div>
`
)
`;
})
.join("");
}
}
@@ -505,7 +569,11 @@
loadHomepageSettings();
loadFeaturedProducts();
</script>
<script src="/assets/js/state-manager.js"></script>
<script src="/assets/js/api-client.js"></script>
<script src="/assets/js/notifications.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart-functions.js"></script>
<script src="/assets/js/shopping.js"></script>
</body>
</html>

View File

@@ -1,15 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Art Shop</title>
<script>
// Redirect to home page
window.location.href = '/home.html';
// Redirect to React app
window.location.href = "/app/";
</script>
</head>
<body>
<p>Loading Sky Art Shop...</p>
</body>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<p>Redirecting to Sky Art Shop...</p>
</body>
</html>

View File

@@ -162,44 +162,45 @@
margin-bottom: 30px;
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<a href="/shop.html" class="btn-cart">
<a href="/shop" class="btn-cart">
<i class="bi bi-cart3"></i>
<span class="cart-count">0</span>
</a>
@@ -227,11 +228,11 @@
<div class="footer-section">
<h4>Quick Links</h4>
<ul>
<li><a href="/home.html">Home</a></li>
<li><a href="/shop.html">Shop</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/home">Home</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
@@ -301,7 +302,7 @@
<i class="bi bi-exclamation-triangle"></i>
<h2>Oops! Something went wrong</h2>
<p>${escapeHtml(message)}</p>
<a href="/home.html" class="btn btn-primary">
<a href="/home" class="btn btn-primary">
<i class="bi bi-house"></i> Back to Home
</a>
</div>

View File

@@ -12,44 +12,45 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1735692200" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link active">Portfolio</a>
<a href="/portfolio" class="nav-link active">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
<a href="/blog" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
@@ -75,7 +76,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -104,10 +105,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -122,18 +123,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -147,7 +148,7 @@
<section
class="portfolio-section"
style="padding: 60px 0; background: #f8f9fa"
style="padding: 60px 0; background: #ffebeb"
>
<div class="container">
<div id="loadingMessage" style="text-align: center; padding: 40px">
@@ -202,28 +203,28 @@
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
@@ -303,7 +304,8 @@
</div>
</div>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
@@ -321,7 +323,7 @@
modalContent.innerHTML = `
<div class="project-image" style="width: 100%; height: 450px; overflow: hidden; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); flex-shrink: 0;">
<img src="${project.imageurl || "/assets/images/placeholder.jpg"}"
<img src="${project.imageurl || "/assets/images/placeholder.svg"}"
alt="${project.title}"
style="width: 100%; height: 100%; object-fit: cover;" />
</div>
@@ -400,7 +402,7 @@
}')" style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s; cursor: pointer;">
<div class="product-image" style="position: relative; padding-top: 100%; overflow: hidden; background: #f5f5f5;">
<img src="${
project.imageurl || "/assets/images/placeholder.jpg"
project.imageurl || "/assets/images/placeholder.svg"
}"
alt="${project.title}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s;"

View File

@@ -20,25 +20,27 @@
<link rel="stylesheet" href="/assets/css/shopping.css" />
<style>
.privacy-hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 80px 0 60px;
color: white;
background: linear-gradient(135deg, #f6ccde 0%, #fcb1d8 100%);
padding: 40px 0 30px;
color: #202023;
text-align: center;
}
.privacy-hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
color: #202023;
}
.privacy-hero p {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
color: #202023;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
.privacy-content {
padding: 60px 0;
background: white;
background: #ffebeb;
}
.privacy-text {
max-width: 900px;
@@ -46,23 +48,25 @@
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2);
line-height: 1.8;
border: 1px solid #ffd0d0;
}
.privacy-text h2 {
color: #333;
color: #202023;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.privacy-text h3 {
color: #555;
color: #202023;
margin-top: 25px;
margin-bottom: 12px;
font-weight: 600;
}
.privacy-text p {
color: #666;
color: #202023;
opacity: 0.8;
margin-bottom: 15px;
}
.privacy-text ul {
@@ -71,7 +75,8 @@
}
.privacy-text li {
margin-bottom: 8px;
color: #666;
color: #202023;
opacity: 0.8;
}
@keyframes spin {
0% {
@@ -82,6 +87,7 @@
}
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
@@ -90,11 +96,11 @@
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
@@ -189,7 +195,7 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
@@ -257,9 +263,9 @@
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
@@ -274,10 +280,10 @@
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="/privacy.html">Privacy Policy</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>

View File

@@ -14,45 +14,87 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/main.css?v=1735692100" />
<link rel="stylesheet" href="/assets/css/navbar.css?v=1735692200" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/responsive.css" />
<style>
/* Custom Scrollbar for Description Box */
div[style*="overflow-y: auto"] {
scrollbar-width: thin;
scrollbar-color: #6b46c1 #f3f4f6;
}
div[style*="overflow-y: auto"]::-webkit-scrollbar {
width: 8px;
}
div[style*="overflow-y: auto"]::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 4px;
}
div[style*="overflow-y: auto"]::-webkit-scrollbar-thumb {
background: #6b46c1;
border-radius: 4px;
}
div[style*="overflow-y: auto"]::-webkit-scrollbar-thumb:hover {
background: #5936a3;
}
/* Smooth image transitions */
#primaryImage {
transition: opacity 0.3s ease-in-out;
}
/* Thumbnail hover effect */
.thumbnail-item:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Color variant animation */
.color-variant-circle {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<a href="/home" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
<a href="/home" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
<a href="/shop" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
<a href="/portfolio" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
<a href="/blog" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
</div>
@@ -78,7 +120,7 @@
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
<a href="/shop" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
@@ -107,10 +149,10 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
<a href="/checkout" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
<a href="/shop" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
@@ -125,18 +167,18 @@
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
<li><a href="/home" class="mobile-link">Home</a></li>
<li><a href="/shop" class="mobile-link">Shop</a></li>
<li><a href="/portfolio" class="mobile-link">Portfolio</a></li>
<li><a href="/about" class="mobile-link">About</a></li>
<li><a href="/blog" class="mobile-link">Blog</a></li>
<li><a href="/contact" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
@@ -146,7 +188,8 @@
text-align: center;
padding: 100px 20px;
font-size: 18px;
color: #6b7280;
color: #202023;
background: #ffebeb;
"
>
<i
@@ -158,28 +201,42 @@
<div id="productDetail" style="display: none"></div>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script src="/assets/js/page-transitions.js?v=1766709739"></script>
<script src="/assets/js/back-button-control.js?v=1766723554"></script>
<script src="/assets/js/main.js?v=1766708114"></script>
<script src="/assets/js/navigation.js?v=1766708114"></script>
<script src="/assets/js/state-manager.js"></script>
<script src="/assets/js/api-client.js"></script>
<script src="/assets/js/notifications.js"></script>
<script src="/assets/js/cart-functions.js"></script>
<script src="/assets/js/cart.js?v=1766708114"></script>
<script src="/assets/js/shopping.js?v=1766708114"></script>
<script>
// Function to change primary image
function changePrimaryImage(imageUrl) {
function changePrimaryImage(imageUrl, index) {
const primaryImg = document.getElementById("primaryImage");
if (primaryImg) {
primaryImg.src = imageUrl;
}
// Update gallery thumbnails border
const galleryImages = document.querySelectorAll(
'[onclick^="changePrimaryImage"]'
);
galleryImages.forEach((img) => {
if (img.src.includes(imageUrl)) {
img.style.border = "3px solid #6b46c1";
// Update thumbnail borders
const thumbnails = document.querySelectorAll(".thumbnail-item");
thumbnails.forEach((thumb, idx) => {
if (idx === index) {
thumb.style.border = "2px solid #6b46c1";
} else {
img.style.border = "1px solid #e5e7eb";
thumb.style.border = "1px solid #e5e7eb";
}
});
// Update color variant borders
const variants = document.querySelectorAll(".color-variant-circle");
variants.forEach((variant) => {
const onclick = variant.getAttribute("onclick");
if (onclick && onclick.includes(imageUrl)) {
variant.style.border = "3px solid #6b46c1";
} else {
variant.style.border = "3px solid #e5e7eb";
}
});
}
@@ -188,15 +245,24 @@
const params = new URLSearchParams(window.location.search);
const productId = params.get("id");
console.log("Product page loaded. URL:", window.location.href);
console.log("Product ID from URL:", productId);
if (!productId) {
console.error("No product ID in URL");
document.getElementById("loading").innerHTML =
'<p>Product not found</p><a href="/shop.html">Back to Shop</a>';
'<p style="text-align: center; padding: 40px; color: #ef4444; font-size: 18px;">Product not found - No product ID in URL</p><div style="text-align: center;"><a href="/shop" style="color: #FCB1D8; text-decoration: none; font-weight: 600;">← Back to Shop</a></div>';
return;
}
try {
console.log(
"Fetching product from API:",
`/api/products/${productId}`
);
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
console.log("API response:", data);
if (!data.success || !data.product) {
throw new Error("Product not found");
@@ -206,7 +272,7 @@
document.title = `${product.name} - Sky Art Shop`;
// Get primary image or first image from images array
let primaryImage = "/assets/images/placeholder.jpg";
let primaryImage = "/assets/images/placeholder.svg";
let imageGallery = [];
if (
@@ -240,7 +306,7 @@
? "3px solid #6b46c1"
: "1px solid #e5e7eb"
};"
onerror="this.src='/assets/images/placeholder.jpg'" />
onerror="this.src='/assets/images/placeholder.svg'" />
`
)
.join("")}
@@ -257,12 +323,12 @@
product.material
) {
detailsHTML = `
<div style="margin-bottom: 24px; padding: 20px; background: #f9fafb; border-radius: 8px;">
<h3 style="font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px;">Product Details</h3>
<div style="margin-bottom: 24px; padding: 20px; background: #FFD0D0; border-radius: 8px; border: 1px solid #FCB1D8;">
<h3 style="font-size: 16px; font-weight: 600; color: #202023; margin-bottom: 16px;">Product Details</h3>
${
product.sku
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<p style="margin-bottom: 8px; color: #202023; opacity: 0.8;">
<span style="font-weight: 500;">SKU:</span> ${product.sku}
</p>
`
@@ -271,7 +337,7 @@
${
product.weight
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<p style="margin-bottom: 8px; color: #202023; opacity: 0.8;">
<span style="font-weight: 500;">Weight:</span> ${product.weight}
</p>
`
@@ -280,7 +346,7 @@
${
product.dimensions
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<p style="margin-bottom: 8px; color: #202023; opacity: 0.8;">
<span style="font-weight: 500;">Dimensions:</span> ${product.dimensions}
</p>
`
@@ -307,7 +373,7 @@
${
product.isfeatured
? `
<span style="display: inline-block; padding: 6px 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 6px; font-size: 12px; font-weight: 600;">
<span style="display: inline-block; padding: 6px 12px; background: #FCB1D8; color: #202023; border-radius: 6px; font-size: 12px; font-weight: 600; box-shadow: 0 2px 4px rgba(252, 177, 216, 0.4);">
<i class="bi bi-star-fill"></i> Featured
</span>
`
@@ -316,7 +382,7 @@
${
product.isbestseller
? `
<span style="display: inline-block; padding: 6px 12px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; border-radius: 6px; font-size: 12px; font-weight: 600;">
<span style="display: inline-block; padding: 6px 12px; background: #F6CCDE; color: #202023; border-radius: 6px; font-size: 12px; font-weight: 600; box-shadow: 0 2px 4px rgba(252, 177, 216, 0.4);">
<i class="bi bi-trophy-fill"></i> Best Seller
</span>
`
@@ -328,51 +394,138 @@
document.getElementById("productDetail").innerHTML = `
<div style="font-family: 'Roboto', sans-serif;">
<nav style="background: white; padding: 16px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<div style="max-width: 1200px; margin: 0 auto; display: flex; align-items: center; gap: 20px;">
<a href="/home.html" style="font-size: 20px; font-weight: 600; color: #1a1a1a; text-decoration: none;">Sky Art Shop</a>
<span style="color: #d1d5db;">/</span>
<a href="/shop.html" style="color: #6b7280; text-decoration: none;">Shop</a>
<span style="color: #d1d5db;">/</span>
<span style="color: #6b7280;">${product.name}</span>
<nav style="background: #F6CCDE; padding: 16px 24px; box-shadow: 0 1px 3px rgba(252, 177, 216, 0.3);">
<div style="max-width: 1400px; margin: 0 auto; display: flex; align-items: center; gap: 20px;">
<a href="/home" style="font-size: 20px; font-weight: 600; color: #202023; text-decoration: none;">Sky Art Shop</a>
<span style="color: #202023; opacity: 0.5;">/</span>
<a href="/shop" style="color: #202023; opacity: 0.8; text-decoration: none; transition: opacity 0.3s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'">Shop</a>
<span style="color: #202023; opacity: 0.5;">/</span>
<span style="color: #202023; opacity: 0.7;">${
product.name
}</span>
</div>
</nav>
<div style="max-width: 1200px; margin: 40px auto; padding: 0 24px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 60px; margin-bottom: 60px;">
<div style="max-width: 1400px; margin: 40px auto; padding: 0 24px; background: #FFEBEB; border-radius: 12px; padding: 40px 24px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 60px;">
<!-- LEFT COLUMN: Image & Description -->
<div>
<div style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<img id="primaryImage"
src="${primaryImage}"
alt="${product.name}"
style="width: 100%; height: auto; display: block;"
onerror="this.src='/assets/images/placeholder.jpg'" />
</div>
${galleryHTML}
${
imageGallery.length > 0 &&
imageGallery.some((img) => img.color_variant)
? `
<div style="margin-top: 16px;">
<p style="font-size: 14px; font-weight: 500; color: #6b7280; margin-bottom: 8px;">Available Colors:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${imageGallery
.filter((img) => img.color_variant)
.map(
(img) => `
<span style="display: inline-block; padding: 6px 12px; background: #f3f4f6; border-radius: 6px; font-size: 13px; color: #1a1a1a;">
${img.color_variant}
</span>
`
)
.join("")}
</div>
<!-- Back to Shop Link -->
<a href="/shop" style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 20px; color: #FCB1D8; text-decoration: none; font-weight: 600; font-size: 15px; transition: all 0.3s;"
onmouseover="this.style.gap='12px'; this.style.color='#d896c0'"
onmouseout="this.style.gap='8px'; this.style.color='#FCB1D8'">
<i class="bi bi-arrow-left"></i> Back to Shop
</a>
<!-- Image Section with Thumbnails -->
<div style="display: flex; gap: 16px;">
<!-- Thumbnail Gallery (Vertical Left) -->
${
imageGallery.length > 1
? `
<div id="thumbnailGallery" style="display: flex; flex-direction: column; gap: 12px; max-height: 600px; overflow-y: auto;">
${imageGallery
.map(
(img, idx) => `
<div onclick="changePrimaryImage('${
img.image_url
}', ${idx})"
class="thumbnail-item"
data-index="${idx}"
style="width: 70px; height: 70px; border-radius: 4px; overflow: hidden; cursor: pointer; border: ${
img.image_url === primaryImage
? "2px solid #6b46c1"
: "1px solid #e5e7eb"
}; transition: all 0.3s; flex-shrink: 0;">
<img src="${img.image_url}"
alt="${img.alt_text || product.name}"
style="width: 100%; height: 100%; object-fit: cover;"
onerror="this.src='/assets/images/placeholder.svg'" />
</div>
`
)
.join("")}
</div>
`
: ""
}
<!-- Main Product Image -->
<div style="flex: 1; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2); border: 1px solid #FFD0D0; padding: 20px; display: flex; align-items: center; justify-content: center; min-height: 500px; max-height: 600px;">
<img id="primaryImage"
src="${primaryImage}"
alt="${product.name}"
style="max-width: 100%; max-height: 560px; width: auto; height: auto; object-fit: contain; display: block;"
onerror="this.src='/assets/images/placeholder.svg'" />
</div>
</div>
<!-- Color Variants Section -->
${
imageGallery.some(
(img) => img.color_variant && img.color_code
)
? `
<div style="margin-top: 24px; padding: 20px; background: #FFD0D0; border-radius: 12px;">
<h4 style="font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
<i class="bi bi-palette"></i>
Available Colors (${
imageGallery.filter((img) => img.color_variant).length
})
</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
${imageGallery
.filter((img) => img.color_variant && img.color_code)
.map(
(img, idx) => `
<div onclick="changePrimaryImage('${
img.image_url
}', ${imageGallery.indexOf(img)})"
class="color-variant-circle"
title="${img.color_variant}"
style="position: relative; width: 48px; height: 48px; border-radius: 50%; cursor: pointer; border: 3px solid ${
img.image_url === primaryImage
? "#FCB1D8"
: "#FFEBEB"
}; transition: all 0.3s; overflow: hidden; box-shadow: 0 2px 4px rgba(252, 177, 216, 0.3);"
onmouseover="this.querySelector('.color-name-tooltip').style.opacity='1'; this.querySelector('.color-name-tooltip').style.transform='translateY(0)'; this.style.transform='scale(1.15)'"
onmouseout="this.querySelector('.color-name-tooltip').style.opacity='0'; this.querySelector('.color-name-tooltip').style.transform='translateY(-5px)'; this.style.transform='scale(1)'">
<div style="width: 100%; height: 100%; background: ${
img.color_code
};"></div>
<div class="color-name-tooltip" style="position: absolute; bottom: -35px; left: 50%; transform: translateX(-50%) translateY(-5px); background: rgba(0,0,0,0.9); color: white; padding: 6px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; pointer-events: none; opacity: 0; transition: all 0.3s; z-index: 10;">
${img.color_variant}
</div>
</div>
`
)
.join("")}
</div>
</div>
`
: ""
}
<!-- Description Box -->
${
product.description
? `
<div style="margin-top: 24px; padding: 20px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2); border: 1px solid #FFD0D0;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 2px solid #FCB1D8;">
<i class="bi bi-pin-angle-fill" style="color: #FCB1D8; font-size: 18px;"></i>
<h3 style="font-size: 16px; font-weight: 600; color: #202023; margin: 0;">Description</h3>
</div>
<div style="max-height: 200px; overflow-y: auto; color: #4b5563; line-height: 1.7; font-size: 15px; padding-right: 8px;">
${product.description}
</div>
</div>
`
: ""
}
</div>
<!-- RIGHT COLUMN: Product Info & Actions -->
<div style="padding: 20px 0;">
${badgesHTML}
<h1 style="font-size: 36px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0; line-height: 1.2;">${
@@ -380,13 +533,13 @@
}</h1>
<div style="display: flex; align-items: baseline; gap: 16px; margin-bottom: 24px;">
<p style="font-size: 36px; font-weight: 700; color: #6b46c1; margin: 0;">$${parseFloat(
<p style="font-size: 36px; font-weight: 700; color: #FCB1D8; margin: 0;">$${parseFloat(
product.price
).toFixed(2)}</p>
${
product.stockquantity > 0
? `<span style="color: #10b981; font-weight: 500;">In Stock (${product.stockquantity} available)</span>`
: `<span style="color: #ef4444; font-weight: 500;">Out of Stock</span>`
? `<span style="color: #10b981; font-weight: 500; display: flex; align-items: center; gap: 6px;"><i class="bi bi-check-circle-fill"></i> In Stock (${product.stockquantity} available)</span>`
: `<span style="color: #ef4444; font-weight: 500; display: flex; align-items: center; gap: 6px;"><i class="bi bi-x-circle-fill"></i> Out of Stock</span>`
}
</div>
@@ -398,44 +551,37 @@
: ""
}
${
product.description
? `
<div style="margin-bottom: 24px;">
<h3 style="font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px;">Description</h3>
<div style="color: #6b7280; line-height: 1.7;">${product.description}</div>
</div>
`
: ""
}
${
product.category
? `
<p style="margin-bottom: 16px;">
<p style="margin-bottom: 24px;">
<span style="font-weight: 500; color: #6b7280;">Category:</span>
<span style="display: inline-block; margin-left: 8px; padding: 4px 12px; background: #f3f4f6; border-radius: 6px; font-size: 14px;">${product.category}</span>
<span style="display: inline-block; margin-left: 8px; padding: 6px 14px; background: #FFD0D0; border-radius: 6px; font-size: 14px; color: #202023;">
<i class="bi bi-tag"></i> ${product.category}
</span>
</p>
`
: ""
}
<!-- Product Details (Moved from description area) -->
${detailsHTML}
<div style="display: flex; gap: 12px; margin-top: 32px;">
<!-- Add to Cart & Wishlist Buttons -->
<div style="display: flex; gap: 12px; margin-top: 32px; margin-bottom: 24px;">
<button onclick="addToCart()"
${product.stockquantity <= 0 ? "disabled" : ""}
style="flex: 1; padding: 16px 32px; background: ${
product.stockquantity <= 0 ? "#9ca3af" : "#6b46c1"
}; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: ${
style="padding: 14px 24px; background: ${
product.stockquantity <= 0 ? "#9ca3af" : "#FCB1D8"
}; color: #202023; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: ${
product.stockquantity <= 0 ? "not-allowed" : "pointer"
}; transition: background 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px;"
}; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(252, 177, 216, 0.3);"
onmouseover="if(${
product.stockquantity > 0
}) this.style.background='#5936a3'"
}) this.style.transform='translateY(-2px)'; this.style.background='#F6CCDE'; this.style.boxShadow='0 6px 16px rgba(252, 177, 216, 0.4)'"
onmouseout="if(${
product.stockquantity > 0
}) this.style.background='#6b46c1'">
}) this.style.transform='translateY(0)'; this.style.background='#FCB1D8'; this.style.boxShadow='0 4px 12px rgba(252, 177, 216, 0.3)'">
<i class="bi bi-cart-plus" style="font-size: 20px;"></i>
${
product.stockquantity <= 0
@@ -444,16 +590,25 @@
}
</button>
<button onclick="addToWishlist()"
style="width: 56px; padding: 16px; background: transparent; color: #6b46c1; border: 2px solid #6b46c1; border-radius: 8px; font-size: 20px; cursor: pointer; transition: all 0.2s;"
onmouseover="this.style.background='#f3f0ff'"
onmouseout="this.style.background='transparent'">
style="padding: 14px 20px; background: white; color: #FCB1D8; border: 2px solid #FCB1D8; border-radius: 10px; font-size: 18px; cursor: pointer; transition: all 0.3s;"
onmouseover="this.style.background='#FCB1D8'; this.style.color='white'; this.style.transform='scale(1.05)'"
onmouseout="this.style.background='white'; this.style.color='#FCB1D8'; this.style.transform='scale(1)'">
<i class="bi bi-heart"></i>
</button>
</div>
<a href="/shop.html" style="display: inline-block; margin-top: 24px; color: #6b46c1; text-decoration: none; font-weight: 500;">
<i class="bi bi-arrow-left"></i> Back to Shop
</a>
</div>
</div>
<!-- Related Products Section -->
<div id="relatedProducts" style="margin-top: 60px; padding-top: 40px; border-top: 2px solid #e5e7eb;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px;">
<h2 style="font-size: 28px; font-weight: 700; color: #1a1a1a; margin: 0; display: flex; align-items: center; gap: 12px;">
<i class="bi bi-box-seam" style="color: #6b46c1;"></i>
You May Also Like
</h2>
</div>
<div id="relatedProductsGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 24px;">
<!-- Products will be loaded here -->
</div>
</div>
</div>
@@ -463,12 +618,28 @@
document.getElementById("loading").style.display = "none";
document.getElementById("productDetail").style.display = "block";
// Store product data
window.currentProduct = product;
// Store product data with imageurl for shopping cart
window.currentProduct = {
...product,
imageurl: primaryImage, // Add the primary image URL for cart display
};
// Track viewed product for smart recommendations
trackViewedProduct({
...product,
imageurl: primaryImage,
});
// Load related products
loadRelatedProducts(product.category, product.id);
console.log("Product loaded successfully:", product.name);
} catch (error) {
console.error("Error loading product:", error);
document.getElementById("loading").innerHTML =
'<p style="color: #ef4444;">Error loading product</p><a href="/shop.html" style="color: #6b46c1; text-decoration: none; font-weight: 500;">Back to Shop</a>';
'<div style="text-align: center; padding: 40px;"><p style="color: #ef4444; font-size: 18px; margin-bottom: 16px;">Error loading product</p><p style="color: #6b7280; margin-bottom: 20px;">' +
error.message +
'</p><a href="/shop" style="color: #FCB1D8; text-decoration: none; font-weight: 600;">← Back to Shop</a></div>';
}
}
@@ -484,7 +655,238 @@
}
}
// Track viewed products for smart recommendations
function trackViewedProduct(product) {
try {
let viewedProducts = JSON.parse(
localStorage.getItem("skyart_viewed_products") || "[]"
);
// Remove if already exists (to update timestamp)
viewedProducts = viewedProducts.filter((p) => p.id !== product.id);
// Add to beginning of array
viewedProducts.unshift({
id: product.id,
name: product.name,
category: product.category,
imageurl: product.imageurl,
price: product.price,
viewedAt: new Date().toISOString(),
});
// Keep only last 20 viewed products
viewedProducts = viewedProducts.slice(0, 20);
localStorage.setItem(
"skyart_viewed_products",
JSON.stringify(viewedProducts)
);
} catch (e) {
console.error("Error tracking viewed product:", e);
}
}
// Load related products (same category + recently viewed)
async function loadRelatedProducts(category, currentProductId) {
try {
const container = document.getElementById("relatedProductsGrid");
if (!container) return;
// Show loading
container.innerHTML =
'<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #6b7280;">Loading recommendations...</div>';
// Fetch products from same category
const response = await fetch("/api/products");
const data = await response.json();
if (data.success && data.products) {
let relatedProducts = [];
// Get products from same category (excluding current)
const sameCategoryProducts = data.products.filter(
(p) => p.category === category && p.id !== currentProductId
);
// Get recently viewed products
const viewedProducts = JSON.parse(
localStorage.getItem("skyart_viewed_products") || "[]"
);
const viewedIds = viewedProducts
.map((p) => p.id)
.filter((id) => id !== currentProductId);
const recentlyViewedProducts = data.products.filter(
(p) => viewedIds.includes(p.id) && p.id !== currentProductId
);
// Combine: prioritize same category, then recently viewed
relatedProducts = [...sameCategoryProducts];
recentlyViewedProducts.forEach((p) => {
if (!relatedProducts.find((rp) => rp.id === p.id)) {
relatedProducts.push(p);
}
});
// Shuffle and limit to 4-8 products
relatedProducts = shuffleArray(relatedProducts).slice(0, 8);
if (relatedProducts.length === 0) {
container.innerHTML =
'<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #6b7280;">No related products found.</div>';
return;
}
// Render products
container.innerHTML = relatedProducts
.map((product) => {
// Get product image (primary or first from images array)
let productImage = "/assets/images/placeholder.svg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
const primaryImg = product.images.find(
(img) => img.is_primary
);
productImage = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageurl) {
productImage = product.imageurl;
}
return `
<a href="/product?id=${
product.id
}" style="text-decoration: none; color: inherit; display: block; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: all 0.3s; border: 1px solid #e5e7eb;"
onmouseover="this.style.transform='translateY(-4px)'; this.style.boxShadow='0 8px 16px rgba(0,0,0,0.12)'"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'">
<div style="aspect-ratio: 1; overflow: hidden; background: #f9fafb;">
<img src="${productImage}"
alt="${product.name}"
style="width: 100%; height: 100%; object-fit: cover;"
onerror="this.src='/assets/images/placeholder.svg'" />
</div>
<div style="padding: 16px;">
<h3 style="font-size: 16px; font-weight: 600; color: #1a1a1a; margin: 0 0 8px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
${product.name}
</h3>
${
product.shortdescription || product.description
? `
<p style="font-size: 14px; color: #636e72; margin: 0 0 12px 0; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
${
product.shortdescription ||
(product.description
? product.description.substring(0, 80) + "..."
: "")
}
</p>
`
: ""
}
${
product.category
? `
<p style="font-size: 13px; color: #6b7280; margin: 0 0 12px 0;">
<i class="bi bi-tag"></i> ${product.category}
</p>
`
: ""
}
<div style="display: flex; align-items: center; justify-content: space-between;">
<p style="font-size: 20px; font-weight: 700; color: #6b46c1; margin: 0;">
$${parseFloat(product.price).toFixed(2)}
</p>
${
product.stockquantity > 0
? '<span style="font-size: 12px; color: #10b981; font-weight: 500;"><i class="bi bi-check-circle-fill"></i> In Stock</span>'
: '<span style="font-size: 12px; color: #ef4444; font-weight: 500;"><i class="bi bi-x-circle-fill"></i> Out of Stock</span>'
}
</div>
</div>
</a>
`;
})
.join("");
}
} catch (error) {
console.error("Error loading related products:", error);
const container = document.getElementById("relatedProductsGrid");
if (container) {
container.innerHTML =
'<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #ef4444;">Error loading recommendations.</div>';
}
}
}
// Shuffle array utility
function shuffleArray(array) {
const arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
loadProduct();
</script>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h3 class="footer-title" id="footerSiteName">Sky Art Shop</h3>
<p class="footer-text">
Your destination for unique art pieces and creative supplies.
</p>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
<a href="#" class="social-link"
><i class="bi bi-instagram"></i
></a>
<a href="#" class="social-link"><i class="bi bi-twitter"></i></a>
<a href="#" class="social-link"
><i class="bi bi-pinterest"></i
></a>
</div>
</div>
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about">Our Story</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p id="footerText">&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

340
website/public/returns.html Normal file
View File

@@ -0,0 +1,340 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Returns & Refunds - Sky Art Shop</title>
<meta name="description" content="Our return policy and refund process" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<style>
.privacy-hero {
background: linear-gradient(135deg, #f6ccde 0%, #fcb1d8 100%);
padding: 40px 0 30px;
color: #202023;
text-align: center;
}
.privacy-hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
color: #202023;
}
.privacy-hero p {
font-size: 1.1rem;
color: #202023;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
.privacy-content {
padding: 60px 0;
background: #ffebeb;
}
.privacy-text {
max-width: 900px;
margin: 0 auto;
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2);
line-height: 1.8;
border: 1px solid #ffd0d0;
}
.privacy-text h2 {
color: #202023;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.privacy-text h3 {
color: #202023;
margin-top: 25px;
margin-bottom: 12px;
font-weight: 600;
}
.privacy-text p {
color: #202023;
opacity: 0.8;
margin-bottom: 15px;
}
.privacy-text ul {
margin-bottom: 20px;
padding-left: 30px;
}
.privacy-text li {
margin-bottom: 8px;
color: #202023;
opacity: 0.8;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<div class="action-item wishlist-dropdown-wrapper">
<button
class="action-btn"
id="wishlistToggle"
aria-label="Wishlist"
>
<i class="bi bi-heart"></i>
<span class="action-badge" id="wishlistCount">0</span>
</button>
<div class="action-dropdown wishlist-dropdown" id="wishlistPanel">
<div class="dropdown-head">
<h3>My Wishlist</h3>
<button class="dropdown-close" id="wishlistClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="wishlistContent">
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
<div class="action-item cart-dropdown-wrapper">
<button
class="action-btn"
id="cartToggle"
aria-label="Shopping Cart"
>
<i class="bi bi-cart3"></i>
<span class="action-badge" id="cartCount">0</span>
</button>
<div class="action-dropdown cart-dropdown" id="cartPanel">
<div class="dropdown-head">
<h3>Shopping Cart</h3>
<button class="dropdown-close" id="cartClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="cartContent">
<p class="empty-state">Your cart is empty</p>
</div>
<div class="dropdown-foot">
<div class="cart-summary">
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
<button class="mobile-toggle" id="mobileMenuToggle" aria-label="Menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
<section class="privacy-hero">
<div class="container">
<h1>Privacy Policy</h1>
<p>Your privacy is important to us</p>
</div>
</section>
<section class="privacy-content">
<div class="container">
<div class="privacy-text" id="privacyContent">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading privacy policy...</p>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h3 class="footer-title">Sky Art Shop</h3>
<p class="footer-text">
Your destination for unique art pieces and creative supplies.
</p>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
<a href="#" class="social-link"
><i class="bi bi-instagram"></i
></a>
<a href="#" class="social-link"><i class="bi bi-twitter"></i></a>
<a href="#" class="social-link"
><i class="bi bi-pinterest"></i
></a>
</div>
</div>
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Load privacy policy content from API
async function loadReturnsContent() {
try {
const response = await fetch("/api/pages/returns");
const data = await response.json();
if (data.success && data.page) {
const contentDiv = document.getElementById("privacyContent");
contentDiv.innerHTML =
data.page.content || "<p>Content not available.</p>";
// Update meta tags if available
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
} else {
document.getElementById("privacyContent").innerHTML =
"<p>Unable to load content.</p>";
}
} catch (error) {
console.error("Error loading privacy content:", error);
document.getElementById("privacyContent").innerHTML =
"<p>Error loading content.</p>";
}
}
// Load content when page loads
document.addEventListener("DOMContentLoaded", loadReturnsContent);
</script>
</body>
</html>

View File

@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shipping Information - Sky Art Shop</title>
<meta
name="description"
content="Shipping methods, delivery times, and policies"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<style>
.privacy-hero {
background: linear-gradient(135deg, #f6ccde 0%, #fcb1d8 100%);
padding: 40px 0 30px;
color: #202023;
text-align: center;
}
.privacy-hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
color: #202023;
}
.privacy-hero p {
font-size: 1.1rem;
color: #202023;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
.privacy-content {
padding: 60px 0;
background: #ffebeb;
}
.privacy-text {
max-width: 900px;
margin: 0 auto;
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(252, 177, 216, 0.2);
line-height: 1.8;
border: 1px solid #ffd0d0;
}
.privacy-text h2 {
color: #202023;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.privacy-text h3 {
color: #202023;
margin-top: 25px;
margin-bottom: 12px;
font-weight: 600;
}
.privacy-text p {
color: #202023;
opacity: 0.8;
margin-bottom: 15px;
}
.privacy-text ul {
margin-bottom: 20px;
padding-left: 30px;
}
.privacy-text li {
margin-bottom: 8px;
color: #202023;
opacity: 0.8;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<link rel="stylesheet" href="/assets/css/theme-colors.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/cat-png-1767324141436-368259437.png"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky' Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<div class="action-item wishlist-dropdown-wrapper">
<button
class="action-btn"
id="wishlistToggle"
aria-label="Wishlist"
>
<i class="bi bi-heart"></i>
<span class="action-badge" id="wishlistCount">0</span>
</button>
<div class="action-dropdown wishlist-dropdown" id="wishlistPanel">
<div class="dropdown-head">
<h3>My Wishlist</h3>
<button class="dropdown-close" id="wishlistClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="wishlistContent">
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
<div class="action-item cart-dropdown-wrapper">
<button
class="action-btn"
id="cartToggle"
aria-label="Shopping Cart"
>
<i class="bi bi-cart3"></i>
<span class="action-badge" id="cartCount">0</span>
</button>
<div class="action-dropdown cart-dropdown" id="cartPanel">
<div class="dropdown-head">
<h3>Shopping Cart</h3>
<button class="dropdown-close" id="cartClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="cartContent">
<p class="empty-state">Your cart is empty</p>
</div>
<div class="dropdown-foot">
<div class="cart-summary">
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
<button class="mobile-toggle" id="mobileMenuToggle" aria-label="Menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky' Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
<section class="privacy-hero">
<div class="container">
<h1>Privacy Policy</h1>
<p>Your privacy is important to us</p>
</div>
</section>
<section class="privacy-content">
<div class="container">
<div class="privacy-text" id="privacyContent">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading privacy policy...</p>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h3 class="footer-title">Sky Art Shop</h3>
<p class="footer-text">
Your destination for unique art pieces and creative supplies.
</p>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
<a href="#" class="social-link"
><i class="bi bi-instagram"></i
></a>
<a href="#" class="social-link"><i class="bi bi-twitter"></i></a>
<a href="#" class="social-link"
><i class="bi bi-pinterest"></i
></a>
</div>
</div>
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop?category=paintings">Paintings</a></li>
<li><a href="/shop?category=prints">Prints</a></li>
<li><a href="/shop?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/faq">FAQ</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Load privacy policy content from API
async function loadShippingContent() {
try {
const response = await fetch("/api/pages/shipping-info");
const data = await response.json();
if (data.success && data.page) {
const contentDiv = document.getElementById("privacyContent");
contentDiv.innerHTML =
data.page.content || "<p>Content not available.</p>";
// Update meta tags if available
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
} else {
document.getElementById("privacyContent").innerHTML =
"<p>Unable to load content.</p>";
}
} catch (error) {
console.error("Error loading privacy content:", error);
document.getElementById("privacyContent").innerHTML =
"<p>Error loading content.</p>";
}
}
// Load content when page loads
document.addEventListener("DOMContentLoaded", loadShippingContent);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,269 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Pages Test - Sky Art Shop</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<style>
body {
padding: 40px;
background: #f8f9fa;
}
.test-card {
background: white;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.test-result {
padding: 15px;
border-radius: 6px;
margin-top: 15px;
font-family: monospace;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.page-list {
list-style: none;
padding: 0;
}
.page-list li {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">
<i class="bi bi-clipboard-check"></i> Custom Pages System Test
</h1>
<div class="test-card">
<h3><i class="bi bi-list-ul"></i> Available Custom Pages</h3>
<p class="text-muted">
These pages are published and visible on the frontend:
</p>
<ul class="page-list" id="pagesList">
<li class="text-center"><em>Loading...</em></li>
</ul>
</div>
<div class="test-card">
<h3><i class="bi bi-link-45deg"></i> Quick Links</h3>
<div class="d-grid gap-2">
<a href="/admin/pages.html" class="btn btn-primary" target="_blank">
<i class="bi bi-gear"></i> Open Admin Pages Manager
</a>
<button class="btn btn-success" onclick="createTestPage()">
<i class="bi bi-plus-circle"></i> Create Test Page
</button>
<button class="btn btn-info" onclick="loadPages()">
<i class="bi bi-arrow-clockwise"></i> Refresh Page List
</button>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-terminal"></i> API Response</h3>
<div
id="apiResponse"
class="test-result success"
style="display: none"
></div>
</div>
</div>
<script>
let pagesData = [];
document.addEventListener("DOMContentLoaded", function () {
loadPages();
});
async function loadPages() {
try {
const response = await fetch("/api/pages");
const data = await response.json();
if (data.success && data.pages) {
pagesData = data.pages;
displayPages(data.pages);
showResult(
"API Response: " + JSON.stringify(data, null, 2),
"success"
);
} else {
showResult(
"Failed to load pages: " + JSON.stringify(data),
"error"
);
}
} catch (error) {
showResult("Error loading pages: " + error.message, "error");
}
}
function displayPages(pages) {
const list = document.getElementById("pagesList");
if (pages.length === 0) {
list.innerHTML =
'<li class="text-center text-muted"><em>No published pages found</em></li>';
return;
}
list.innerHTML = pages
.map(
(page) => `
<li>
<div>
<strong>${escapeHtml(page.title)}</strong>
<br>
<small class="text-muted">Slug: ${escapeHtml(
page.slug
)} | Created: ${new Date(
page.createdat
).toLocaleDateString()}</small>
</div>
<div>
<a href="/page.html?slug=${encodeURIComponent(
page.slug
)}" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="bi bi-eye"></i> View
</a>
</div>
</li>
`
)
.join("");
}
async function createTestPage() {
const title = "Test Page " + Date.now();
const slug = "test-page-" + Date.now();
const testContent = {
ops: [
{ insert: "Welcome to the Test Page", attributes: { header: 1 } },
{ insert: "\n\nThis is a test page created automatically. " },
{
insert: "It contains formatted text",
attributes: { bold: true },
},
{ insert: " with " },
{ insert: "different styles", attributes: { italic: true } },
{ insert: ".\n\n" },
{ insert: "Key Features:", attributes: { header: 2 } },
{ insert: "\n" },
{
insert: "Rich text editing with Quill",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Create and edit pages", attributes: { list: "bullet" } },
{ insert: "\n" },
{ insert: "Delete pages", attributes: { list: "bullet" } },
{ insert: "\n" },
{ insert: "Display on frontend", attributes: { list: "bullet" } },
{ insert: "\n" },
],
};
const testHTML = `
<h1>Welcome to the Test Page</h1>
<p>This is a test page created automatically. <strong>It contains formatted text</strong> with <em>different styles</em>.</p>
<h2>Key Features:</h2>
<ul>
<li>Rich text editing with Quill</li>
<li>Create and edit pages</li>
<li>Delete pages</li>
<li>Display on frontend</li>
</ul>
`;
try {
// Note: This will fail without authentication
const response = await fetch("/api/admin/pages", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
title: title,
slug: slug,
content: JSON.stringify(testContent),
contenthtml: testHTML,
metatitle: title,
metadescription: "This is a test page",
ispublished: true,
}),
});
const data = await response.json();
if (data.success) {
showResult(
"Test page created successfully! ID: " + data.page.id,
"success"
);
loadPages();
} else {
showResult(
"Failed to create test page. You may need to be logged in as admin. Error: " +
(data.message || "Unknown error"),
"error"
);
}
} catch (error) {
showResult(
"Error creating test page: " +
error.message +
". Make sure you are logged in as admin.",
"error"
);
}
}
function showResult(message, type) {
const result = document.getElementById("apiResponse");
result.textContent = message;
result.className = "test-result " + type;
result.style.display = "block";
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
</script>
</body>
</html>

View File

@@ -1,249 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend-Frontend Data Sync Test</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<style>
body {
padding: 40px;
background: #f8f9fa;
}
.test-card {
background: white;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
.preview-box {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
max-height: 400px;
overflow-y: auto;
}
.step {
padding: 15px;
margin: 10px 0;
border-left: 4px solid #667eea;
background: #f8f9fa;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">
<i class="bi bi-arrow-repeat"></i> Backend-Frontend Sync Test
</h1>
<div class="test-card">
<h3>
<i class="bi bi-check-circle-fill text-success"></i>
Data Communication Status
</h3>
<p class="text-muted mb-3">
Testing the connection between admin panel edits and frontend display
</p>
<div class="step">
<strong>Step 1:</strong> Open Admin Panel →
<a
href="/admin/pages.html"
target="_blank"
class="btn btn-sm btn-primary"
>
<i class="bi bi-gear"></i> Open Pages Admin
</a>
</div>
<div class="step">
<strong>Step 2:</strong> Click Edit on any page (About, Contact, or
Privacy)
</div>
<div class="step">
<strong>Step 3:</strong> Make a small change (e.g., update phone
number, add text)
</div>
<div class="step">
<strong>Step 4:</strong> Click "Save Page" in the admin modal
</div>
<div class="step">
<strong>Step 5:</strong> Return to this test page and click the
buttons below to verify
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-eye"></i> Live Page Previews</h3>
<p class="text-muted">
View current content from database (click to refresh)
</p>
<div class="row g-3">
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('about')"
>
<i class="bi bi-file-text"></i> Test About Page
</button>
<a href="/about.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('contact')"
>
<i class="bi bi-envelope"></i> Test Contact Page
</button>
<a href="/contact.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('privacy')"
>
<i class="bi bi-shield-check"></i> Test Privacy Page
</button>
<a href="/privacy.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
</div>
<div id="previewContainer" style="display: none">
<hr class="my-4" />
<h4 id="previewTitle">Content Preview</h4>
<span class="status-badge status-success mb-3">
<i class="bi bi-check-circle"></i> Loaded from Database
</span>
<div class="preview-box" id="previewContent"></div>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-clipboard-data"></i> Test Results</h3>
<div id="testResults">
<p class="text-muted">
<i class="bi bi-info-circle"></i>
Click a test button above to check if data is syncing correctly
</p>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-lightbulb"></i> What Should Happen</h3>
<ul>
<li>
<strong>Edit in Admin</strong>: Changes saved to database
immediately
</li>
<li>
<strong>View on Frontend</strong>: Refresh page shows updated
content
</li>
<li>
<strong>No Cache Issues</strong>: Changes appear within seconds
</li>
<li>
<strong>All Sections Updated</strong>: Headers, paragraphs, lists
all reflect edits
</li>
</ul>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle-fill"></i>
<strong>Pro Tip:</strong> Keep this test page and the frontend page
open side-by-side. Edit in admin, save, then refresh the frontend page
to see changes instantly.
</div>
</div>
</div>
<script>
async function testPage(slug) {
const previewContainer = document.getElementById("previewContainer");
const previewTitle = document.getElementById("previewTitle");
const previewContent = document.getElementById("previewContent");
const testResults = document.getElementById("testResults");
previewContainer.style.display = "block";
previewTitle.textContent = `Loading ${slug} page...`;
previewContent.innerHTML =
'<div class="text-center"><div class="spinner-border" role="status"></div></div>';
try {
const response = await fetch(`/api/pages/${slug}`);
const data = await response.json();
if (data.success && data.page) {
previewTitle.textContent = `${data.page.title} - Content Preview`;
previewContent.innerHTML = data.page.content;
testResults.innerHTML = `
<div class="alert alert-success">
<h5><i class="bi bi-check-circle-fill"></i> ✓ Communication Working!</h5>
<p><strong>Page:</strong> ${data.page.title}</p>
<p><strong>Slug:</strong> ${data.page.slug}</p>
<p><strong>Content Length:</strong> ${data.page.content.length} characters</p>
<p class="mb-0"><strong>Status:</strong> Data successfully loaded from database</p>
<hr>
<small class="text-muted">
<i class="bi bi-info-circle"></i>
Any edits you make in the admin panel will be reflected here after saving and refreshing.
</small>
</div>
`;
} else {
throw new Error("Page not found");
}
} catch (error) {
previewContent.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle-fill"></i> Error loading content: ${error.message}
</div>
`;
testResults.innerHTML = `
<div class="alert alert-danger">
<h5><i class="bi bi-x-circle-fill"></i> ✗ Communication Error</h5>
<p>${error.message}</p>
</div>
`;
}
}
</script>
</body>
</html>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Products Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.product-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
.product-card h3 {
margin: 10px 0;
font-size: 16px;
}
.product-card .price {
color: #ff6b6b;
font-weight: bold;
font-size: 18px;
}
.loading {
text-align: center;
padding: 40px;
}
</style>
</head>
<body>
<h1>Product API Test</h1>
<div id="status" class="loading">Loading products...</div>
<div id="products" class="product-grid"></div>
<script>
async function testProducts() {
const statusDiv = document.getElementById("status");
const productsDiv = document.getElementById("products");
try {
console.log("Fetching products from /api/products...");
const response = await fetch("/api/products");
console.log("Response status:", response.status);
const data = await response.json();
console.log("Response data:", data);
if (data.success && data.products) {
const products = data.products;
statusDiv.innerHTML = `<strong>Success!</strong> Loaded ${products.length} products`;
statusDiv.style.color = "green";
productsDiv.innerHTML = products
.map(
(p) => `
<div class="product-card">
<img src="${
p.imageurl || "/assets/images/placeholder.jpg"
}"
alt="${p.name}"
onerror="this.src='/assets/images/placeholder.jpg'">
<h3>${p.name}</h3>
<p>${p.shortdescription || p.description || ""}</p>
<p class="price">$${parseFloat(p.price).toFixed(
2
)}</p>
</div>
`
)
.join("");
console.log("Products rendered successfully");
} else {
statusDiv.innerHTML = "<strong>Error:</strong> No products found";
statusDiv.style.color = "red";
console.error("No products in response");
}
} catch (error) {
statusDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
statusDiv.style.color = "red";
console.error("Error loading products:", error);
}
}
testProducts();
</script>
</body>
</html>

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body style="margin: 0; padding: 20px; background: white;">
<h1>Products Grid Test</h1>
<div id="productsGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px;">
<div style="border: 1px solid #ccc; padding: 16px; border-radius: 8px;">
<img src="/assets/images/products/washi-1.jpg" style="width: 100%; height: 200px; object-fit: cover;" alt="Test">
<h3>Test Product 1</h3>
<p style="color: #ff6b6b; font-weight: bold;">$15.99</p>
</div>
<div style="border: 1px solid #ccc; padding: 16px; border-radius: 8px;">
<img src="/assets/images/products/washi-2.jpg" style="width: 100%; height: 200px; object-fit: cover;" alt="Test">
<h3>Test Product 2</h3>
<p style="color: #ff6b6b; font-weight: bold;">$8.99</p>
</div>
</div>
<script>
console.log('Test page loaded');
</script>
</body>
</html>

View File

@@ -1,324 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Page Structured Fields Test</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<style>
body {
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.test-container {
max-width: 1400px;
margin: 0 auto;
}
.test-card {
background: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.success-badge {
display: inline-block;
background: #d4edda;
color: #155724;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
margin: 5px;
}
.step {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
}
.step strong {
color: #667eea;
}
.split-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
@media (max-width: 968px) {
.split-view {
grid-template-columns: 1fr;
}
}
.preview-frame {
border: 3px solid #667eea;
border-radius: 8px;
min-height: 600px;
background: white;
}
h1,
h2,
h3 {
color: #2d3436;
}
.instruction-badge {
background: #fff3cd;
color: #856404;
padding: 12px 20px;
border-radius: 8px;
border-left: 4px solid #ffc107;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="test-container">
<div class="test-card text-center">
<h1 class="mb-3">
<i class="bi bi-check-circle-fill text-success"></i>
Contact Page Structured Fields
</h1>
<p class="lead">
Test the new structured editing system that prevents layout breaking
</p>
<div class="mt-3">
<span class="success-badge">✓ Layout Protected</span>
<span class="success-badge">✓ Data Separated</span>
<span class="success-badge">✓ User-Friendly</span>
<span class="success-badge">✓ No Errors</span>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-list-check"></i> Testing Steps</h2>
<p class="text-muted mb-4">
Follow these steps to see the structured fields in action
</p>
<div class="step">
<strong>Step 1:</strong> Open the admin panel in a new tab →
<a
href="/admin/pages.html"
target="_blank"
class="btn btn-sm btn-primary ms-2"
>
<i class="bi bi-box-arrow-up-right"></i> Open Admin Panel
</a>
</div>
<div class="step">
<strong>Step 2:</strong> Find the "Contact" page in the list and click
the <strong>Edit</strong> button (pencil icon)
</div>
<div class="step">
<strong>Step 3:</strong> Notice you DON'T see a Quill rich text
editor. Instead, you see:
<ul class="mt-2">
<li>
<strong>Header Section Card</strong> - Title and subtitle fields
</li>
<li>
<strong>Contact Information Card</strong> - Phone, email, address
fields
</li>
<li>
<strong>Business Hours Card</strong> - Multiple time slot fields
with add/remove buttons
</li>
</ul>
</div>
<div class="step">
<strong>Step 4:</strong> Make a change:
<ul class="mt-2">
<li>Change phone number to <code>+1 (555) 999-8888</code></li>
<li>
Or update the header title to <code>Contact Sky Art Shop</code>
</li>
<li>Or add a new business hour slot</li>
</ul>
</div>
<div class="step">
<strong>Step 5:</strong> Click <strong>"Save Page"</strong> button at
the bottom of the modal
</div>
<div class="step">
<strong>Step 6:</strong> Return to this page and click the button
below to refresh the preview:
<button
class="btn btn-sm btn-success mt-2"
onclick="refreshPreview()"
>
<i class="bi bi-arrow-clockwise"></i> Refresh Contact Page Preview
</button>
</div>
<div class="instruction-badge">
<i class="bi bi-lightbulb-fill"></i>
<strong>What to Expect:</strong> The contact page will show your
updated data but the beautiful gradient layout, icons, and styling
will remain perfectly intact!
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-split"></i> Live Comparison</h2>
<p class="text-muted mb-3">
Compare admin interface with frontend result
</p>
<div class="split-view">
<div>
<h4 class="mb-3"><i class="bi bi-gear"></i> Admin Panel</h4>
<iframe
id="adminFrame"
src="/admin/pages.html"
class="preview-frame w-100"
title="Admin Panel"
>
</iframe>
</div>
<div>
<h4 class="mb-3">
<i class="bi bi-eye"></i> Frontend Contact Page
<button
class="btn btn-sm btn-outline-primary"
onclick="refreshPreview()"
>
<i class="bi bi-arrow-clockwise"></i>
</button>
</h4>
<iframe
id="contactFrame"
src="/contact.html"
class="preview-frame w-100"
title="Contact Page"
>
</iframe>
</div>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-shield-check"></i> What's Different?</h2>
<div class="row mt-4">
<div class="col-md-6">
<div class="alert alert-danger">
<h5><i class="bi bi-x-circle"></i> Before (Problem)</h5>
<ul>
<li>Single rich text editor for entire page</li>
<li>User could type anything (e.g., "5")</li>
<li>Would replace entire beautiful layout</li>
<li>Lost gradient cards, icons, styling</li>
<li>Required HTML knowledge to maintain</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="alert alert-success">
<h5><i class="bi bi-check-circle"></i> After (Solution)</h5>
<ul>
<li>Structured input fields for each section</li>
<li>Can only enter data, not HTML</li>
<li>JavaScript generates formatted HTML</li>
<li>Layout template is protected</li>
<li>No HTML knowledge needed</li>
</ul>
</div>
</div>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-database"></i> Technical Details</h2>
<h4 class="mt-4">Database Structure</h4>
<pre class="bg-light p-3 rounded"><code>{
"header": {
"title": "Our Contact Information",
"subtitle": "Reach out to us..."
},
"contactInfo": {
"phone": "+1 (555) 123-4567",
"email": "contact@skyartshop.com",
"address": "123 Art Street..."
},
"businessHours": [
{ "days": "Monday - Friday", "hours": "9:00 AM - 6:00 PM" },
{ "days": "Saturday", "hours": "10:00 AM - 4:00 PM" }
]
}</code></pre>
<h4 class="mt-4">How It Works</h4>
<ol>
<li>
<strong>Admin edits fields</strong> → Structured data collected
</li>
<li>
<strong>JavaScript function</strong> → Generates formatted HTML from
template
</li>
<li>
<strong>Save to database</strong> → Stores both structured data
(JSON) and generated HTML
</li>
<li><strong>Frontend displays</strong> → Shows the generated HTML</li>
<li><strong>Result</strong> → Data changes, layout stays perfect!</li>
</ol>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle-fill"></i>
<strong>Note:</strong> Other pages (About, Privacy) still use the rich
text editor because they don't have a fixed layout requirement. The
system automatically detects which editor to show.
</div>
</div>
<div class="test-card text-center">
<h3 class="mb-3">Quick Links</h3>
<a href="/admin/pages.html" target="_blank" class="btn btn-primary m-2">
<i class="bi bi-gear"></i> Admin Panel
</a>
<a href="/contact.html" target="_blank" class="btn btn-success m-2">
<i class="bi bi-envelope"></i> Contact Page
</a>
<a href="/test-data-sync.html" target="_blank" class="btn btn-info m-2">
<i class="bi bi-arrow-repeat"></i> Data Sync Test
</a>
</div>
</div>
<script>
function refreshPreview() {
const contactFrame = document.getElementById("contactFrame");
contactFrame.src = contactFrame.src; // Reload iframe
// Show feedback
const btn = event.target.closest("button");
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-circle"></i> Refreshed!';
btn.classList.remove("btn-outline-primary", "btn-success");
btn.classList.add("btn-success");
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove("btn-success");
btn.classList.add("btn-outline-primary");
}, 2000);
}
</script>
</body>
</html>