updateweb

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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