updateweb
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
465
website/admin/test-notifications.html
Normal file
465
website/admin/test-notifications.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
358
website/admin/test-user-api.html
Normal file
358
website/admin/test-user-api.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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%; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
439
website/assets/css/responsive-enhanced.css
Normal file
439
website/assets/css/responsive-enhanced.css
Normal 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;
|
||||
}
|
||||
}
|
||||
626
website/assets/css/responsive.css
Normal file
626
website/assets/css/responsive.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
BIN
website/assets/fonts/AmsterdamThreeSlant-axaym.ttf
Normal file
BIN
website/assets/fonts/AmsterdamThreeSlant-axaym.ttf
Normal file
Binary file not shown.
111
website/assets/js/api-client.js
Normal file
111
website/assets/js/api-client.js
Normal 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");
|
||||
}
|
||||
};
|
||||
})();
|
||||
62
website/assets/js/back-button-control.js
Normal file
62
website/assets/js/back-button-control.js
Normal 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);
|
||||
})();
|
||||
155
website/assets/js/cart-functions.js
Normal file
155
website/assets/js/cart-functions.js
Normal 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;
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
|
||||
72
website/assets/js/lazy-load.js
Normal file
72
website/assets/js/lazy-load.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
|
||||
224
website/assets/js/notifications.js
Normal file
224
website/assets/js/notifications.js
Normal 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);
|
||||
};
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
236
website/assets/js/state-manager.js
Normal file
236
website/assets/js/state-manager.js
Normal 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");
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
458
website/public/assets/css/navbar.css
Normal file
458
website/public/assets/css/navbar.css
Normal 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;
|
||||
}
|
||||
}
|
||||
439
website/public/assets/css/responsive-enhanced.css
Normal file
439
website/public/assets/css/responsive-enhanced.css
Normal 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;
|
||||
}
|
||||
}
|
||||
462
website/public/assets/css/theme-colors.css
Normal file
462
website/public/assets/css/theme-colors.css
Normal 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;
|
||||
}
|
||||
0
website/public/assets/images/placeholder.jpg
Normal file
0
website/public/assets/images/placeholder.jpg
Normal file
4
website/public/assets/images/placeholder.svg
Normal file
4
website/public/assets/images/placeholder.svg
Normal 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 |
111
website/public/assets/js/api-client.js
Normal file
111
website/public/assets/js/api-client.js
Normal 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");
|
||||
}
|
||||
};
|
||||
})();
|
||||
62
website/public/assets/js/back-button-control.js
Normal file
62
website/public/assets/js/back-button-control.js
Normal 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);
|
||||
})();
|
||||
155
website/public/assets/js/cart-functions.js
Normal file
155
website/public/assets/js/cart-functions.js
Normal 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;
|
||||
})();
|
||||
319
website/public/assets/js/cart.js
Normal file
319
website/public/assets/js/cart.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
72
website/public/assets/js/lazy-load.js
Normal file
72
website/public/assets/js/lazy-load.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
350
website/public/assets/js/main.js
Normal file
350
website/public/assets/js/main.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
203
website/public/assets/js/navigation.js
Normal file
203
website/public/assets/js/navigation.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
224
website/public/assets/js/notifications.js
Normal file
224
website/public/assets/js/notifications.js
Normal 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);
|
||||
};
|
||||
})();
|
||||
555
website/public/assets/js/page-transitions.js
Normal file
555
website/public/assets/js/page-transitions.js
Normal 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);
|
||||
}
|
||||
306
website/public/assets/js/shopping.js
Normal file
306
website/public/assets/js/shopping.js
Normal 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();
|
||||
}
|
||||
}
|
||||
})();
|
||||
236
website/public/assets/js/state-manager.js
Normal file
236
website/public/assets/js/state-manager.js
Normal 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");
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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
343
website/public/faq.html
Normal 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>© 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">© 2025 Sky Art Shop. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
340
website/public/returns.html
Normal file
340
website/public/returns.html
Normal 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>© 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>
|
||||
343
website/public/shipping-info.html
Normal file
343
website/public/shipping-info.html
Normal 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>© 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
@@ -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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user