Files
SkyArtShop/website/admin/menu.html
Local Server 2a2a3d99e5 webupdate
2026-01-18 02:22:05 -06:00

975 lines
26 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Menu Manager - Sky Art Shop Admin</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"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
:root {
--accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
--hover-shadow: 0 8px 30px rgba(102, 126, 234, 0.2);
--border-radius: 16px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Menu Container */
.menu-container {
display: grid;
grid-template-columns: 1fr 350px;
gap: 30px;
max-width: 1400px;
}
@media (max-width: 992px) {
.menu-container {
grid-template-columns: 1fr;
}
}
/* Menu Items List */
.menu-list-card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
overflow: hidden;
}
.menu-list-header {
background: var(--accent-gradient);
color: white;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-list-header h5 {
margin: 0;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.menu-list-body {
padding: 20px;
min-height: 400px;
}
/* Sortable Menu Items */
.menu-items {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
cursor: grab;
transition: var(--transition);
}
.menu-item:hover {
border-color: #667eea;
box-shadow: var(--card-shadow);
}
.menu-item:active {
cursor: grabbing;
}
.menu-item.dragging {
opacity: 0.5;
border-color: #667eea;
background: #eef2ff;
}
.menu-item.drag-over {
border-color: #667eea;
background: #eef2ff;
}
.drag-handle {
color: #94a3b8;
font-size: 1.2rem;
cursor: grab;
}
.menu-item-icon {
width: 40px;
height: 40px;
background: var(--accent-gradient);
color: white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.menu-item-content {
flex: 1;
}
.menu-item-name {
font-weight: 600;
color: #334155;
margin-bottom: 4px;
}
.menu-item-url {
font-size: 0.85rem;
color: #64748b;
font-family: "Monaco", "Consolas", monospace;
}
.menu-item-actions {
display: flex;
gap: 8px;
}
.menu-item-actions button {
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
}
.btn-edit-item {
background: #e0e7ff;
color: #4f46e5;
}
.btn-edit-item:hover {
background: #4f46e5;
color: white;
}
.btn-delete-item {
background: #fee2e2;
color: #dc2626;
}
.btn-delete-item:hover {
background: #dc2626;
color: white;
}
.visibility-toggle {
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
}
.visibility-toggle.visible {
background: #dcfce7;
color: #166534;
}
.visibility-toggle.hidden {
background: #f1f5f9;
color: #94a3b8;
}
/* Add/Edit Form Card */
.form-card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
overflow: hidden;
position: sticky;
top: 100px;
}
.form-card-header {
background: var(--accent-gradient);
color: white;
padding: 20px 24px;
}
.form-card-header h5 {
margin: 0;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.form-card-body {
padding: 24px;
}
.form-label {
font-weight: 500;
color: #475569;
margin-bottom: 8px;
}
.form-control,
.form-select {
border-radius: 10px;
border: 2px solid #e2e8f0;
padding: 12px 16px;
transition: var(--transition);
}
.form-control:focus,
.form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Icon Picker */
.icon-picker {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
max-height: 200px;
overflow-y: auto;
padding: 12px;
background: #f8fafc;
border-radius: 10px;
border: 2px solid #e2e8f0;
}
.icon-option {
width: 40px;
height: 40px;
border-radius: 8px;
border: 2px solid transparent;
background: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
font-size: 1.1rem;
color: #64748b;
}
.icon-option:hover {
border-color: #667eea;
color: #667eea;
}
.icon-option.selected {
border-color: #667eea;
background: #667eea;
color: white;
}
/* Button Styles */
.btn-save {
background: var(--accent-gradient);
border: none;
color: white;
padding: 12px 24px;
border-radius: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: var(--transition);
width: 100%;
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
color: white;
}
.btn-cancel {
background: #f1f5f9;
border: none;
color: #64748b;
padding: 12px 24px;
border-radius: 10px;
font-weight: 500;
width: 100%;
transition: var(--transition);
}
.btn-cancel:hover {
background: #e2e8f0;
color: #334155;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #64748b;
}
.empty-state i {
font-size: 4rem;
color: #cbd5e1;
margin-bottom: 20px;
}
.empty-state h5 {
margin-bottom: 12px;
}
.empty-state p {
color: #94a3b8;
}
/* Info Card */
.info-card {
background: #eef2ff;
border: 1px solid #c7d2fe;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 20px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.info-card i {
color: #667eea;
font-size: 1.2rem;
margin-top: 2px;
}
.info-card p {
margin: 0;
color: #4338ca;
font-size: 0.9rem;
}
/* Toast */
.toast-container {
z-index: 1100;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu" class="active"
><i class="bi bi-list"></i> Menu</a
>
</li>
<li>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
<div class="main-content">
<div class="top-bar">
<div>
<h3><i class="bi bi-list me-2"></i>Menu Manager</h3>
<p class="mb-0 text-muted">Manage your website navigation menu</p>
</div>
<div>
<button
class="btn btn-outline-secondary me-2"
onclick="loadMenuItems()"
>
<i class="bi bi-arrow-clockwise"></i>
</button>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<div class="info-card">
<i class="bi bi-info-circle"></i>
<p>
Drag and drop menu items to reorder them. Changes are saved
automatically when you reorder items.
</p>
</div>
<div class="menu-container">
<!-- Menu Items List -->
<div class="menu-list-card">
<div class="menu-list-header">
<h5><i class="bi bi-list-ul"></i> Navigation Menu</h5>
<span class="badge bg-white text-dark" id="menuCount">0 items</span>
</div>
<div class="menu-list-body">
<ul class="menu-items" id="menuItemsList">
<!-- Menu items will be loaded here -->
</ul>
</div>
</div>
<!-- Add/Edit Form -->
<div class="form-card">
<div class="form-card-header">
<h5 id="formTitle">
<i class="bi bi-plus-circle"></i> Add Menu Item
</h5>
</div>
<div class="form-card-body">
<form id="menuItemForm">
<input type="hidden" id="menuItemId" />
<div class="mb-3">
<label for="menuItemName" class="form-label"
>Menu Label *</label
>
<input
type="text"
class="form-control"
id="menuItemName"
placeholder="e.g., Products"
required
/>
</div>
<div class="mb-3">
<label for="menuItemUrl" class="form-label">URL / Link *</label>
<input
type="text"
class="form-control"
id="menuItemUrl"
placeholder="e.g., /products"
required
/>
<div class="form-text">
Use relative paths like /products or full URLs
</div>
</div>
<div class="mb-3">
<label class="form-label">Icon</label>
<div class="icon-picker" id="iconPicker">
<!-- Icons will be loaded here -->
</div>
<input type="hidden" id="menuItemIcon" />
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="menuItemVisible"
checked
style="width: 48px; height: 26px"
/>
<label class="form-check-label ms-2" for="menuItemVisible">
<strong>Visible</strong>
<span class="text-muted d-block" style="font-size: 0.85rem"
>Show in navigation menu</span
>
</label>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-save">
<i class="bi bi-check-lg"></i>
<span id="submitBtnText">Add Menu Item</span>
</button>
<button
type="button"
class="btn btn-cancel"
onclick="resetForm()"
id="cancelBtn"
style="display: none"
>
<i class="bi bi-x-lg me-1"></i>Cancel Edit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div
class="toast-container position-fixed bottom-0 end-0 p-3"
id="toastContainer"
></div>
<!-- Scripts -->
<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>
// State
let menuItems = [];
let selectedIcon = "";
let draggedItem = null;
// Available icons
const availableIcons = [
"bi-house",
"bi-box",
"bi-cart",
"bi-bag",
"bi-shop",
"bi-grid",
"bi-collection",
"bi-star",
"bi-heart",
"bi-tag",
"bi-bookmark",
"bi-file-text",
"bi-newspaper",
"bi-camera",
"bi-images",
"bi-easel",
"bi-palette",
"bi-brush",
"bi-pencil",
"bi-person",
"bi-people",
"bi-envelope",
"bi-telephone",
"bi-geo-alt",
"bi-map",
"bi-globe",
"bi-info-circle",
"bi-question-circle",
"bi-shield-check",
"bi-truck",
"bi-arrow-return-left",
"bi-credit-card",
"bi-gift",
"bi-award",
"bi-trophy",
];
// Initialize
document.addEventListener("DOMContentLoaded", () => {
loadMenuItems();
setupIconPicker();
setupFormSubmit();
});
// Load menu items
async function loadMenuItems() {
try {
const response = await fetch("/api/menu", {
credentials: "include",
});
if (!response.ok) throw new Error("Failed to load menu items");
menuItems = await response.json();
renderMenuItems();
} catch (error) {
console.error("Error loading menu:", error);
showToast("Failed to load menu items", "error");
}
}
// Render menu items
function renderMenuItems() {
const list = document.getElementById("menuItemsList");
document.getElementById("menuCount").textContent =
`${menuItems.length} items`;
if (menuItems.length === 0) {
list.innerHTML = `
<div class="empty-state">
<i class="bi bi-menu-button-wide"></i>
<h5>No Menu Items</h5>
<p>Add your first menu item using the form</p>
</div>
`;
return;
}
list.innerHTML = menuItems
.map(
(item, index) => `
<li class="menu-item"
draggable="true"
data-id="${item.id}"
data-index="${index}"
ondragstart="handleDragStart(event, ${index})"
ondragover="handleDragOver(event)"
ondragenter="handleDragEnter(event)"
ondragleave="handleDragLeave(event)"
ondrop="handleDrop(event, ${index})"
ondragend="handleDragEnd(event)">
<span class="drag-handle">
<i class="bi bi-grip-vertical"></i>
</span>
<div class="menu-item-icon">
<i class="bi ${item.icon || "bi-link-45deg"}"></i>
</div>
<div class="menu-item-content">
<div class="menu-item-name">${escapeHtml(item.name)}</div>
<div class="menu-item-url">${escapeHtml(item.url)}</div>
</div>
<div class="menu-item-actions">
<button class="visibility-toggle ${item.isvisible ? "visible" : "hidden"}"
onclick="toggleVisibility(${item.id})"
title="${item.isvisible ? "Click to hide" : "Click to show"}">
<i class="bi ${item.isvisible ? "bi-eye" : "bi-eye-slash"}"></i>
</button>
<button class="btn-edit-item" onclick="editMenuItem(${item.id})" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn-delete-item" onclick="deleteMenuItem(${item.id})" title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</li>
`,
)
.join("");
}
// Setup icon picker
function setupIconPicker() {
const picker = document.getElementById("iconPicker");
picker.innerHTML = availableIcons
.map(
(icon) => `
<div class="icon-option" data-icon="${icon}" onclick="selectIcon('${icon}')">
<i class="bi ${icon}"></i>
</div>
`,
)
.join("");
}
// Select icon
function selectIcon(icon) {
selectedIcon = icon;
document.getElementById("menuItemIcon").value = icon;
document.querySelectorAll(".icon-option").forEach((opt) => {
opt.classList.remove("selected");
});
document
.querySelector(`.icon-option[data-icon="${icon}"]`)
?.classList.add("selected");
}
// Setup form submit
function setupFormSubmit() {
document
.getElementById("menuItemForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const id = document.getElementById("menuItemId").value;
const data = {
name: document.getElementById("menuItemName").value,
url: document.getElementById("menuItemUrl").value,
icon: selectedIcon || "bi-link-45deg",
isvisible: document.getElementById("menuItemVisible").checked,
display_order: id
? menuItems.find((m) => m.id == id)?.display_order
: menuItems.length,
};
try {
const url = id ? `/api/menu/${id}` : "/api/menu";
const method = id ? "PUT" : "POST";
const response = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(data),
});
if (!response.ok) throw new Error("Failed to save menu item");
showToast(
`Menu item ${id ? "updated" : "added"} successfully!`,
"success",
);
resetForm();
loadMenuItems();
} catch (error) {
console.error("Error saving menu item:", error);
showToast("Failed to save menu item", "error");
}
});
}
// Edit menu item
function editMenuItem(id) {
const item = menuItems.find((m) => m.id === id);
if (!item) return;
document.getElementById("menuItemId").value = item.id;
document.getElementById("menuItemName").value = item.name;
document.getElementById("menuItemUrl").value = item.url;
document.getElementById("menuItemVisible").checked = item.isvisible;
if (item.icon) {
selectIcon(item.icon);
}
document.getElementById("formTitle").innerHTML =
'<i class="bi bi-pencil"></i> Edit Menu Item';
document.getElementById("submitBtnText").textContent =
"Update Menu Item";
document.getElementById("cancelBtn").style.display = "block";
// Scroll to form on mobile
document
.querySelector(".form-card")
.scrollIntoView({ behavior: "smooth" });
}
// Delete menu item
async function deleteMenuItem(id) {
if (!confirm("Are you sure you want to delete this menu item?")) return;
try {
const response = await fetch(`/api/menu/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!response.ok) throw new Error("Failed to delete menu item");
showToast("Menu item deleted successfully!", "success");
loadMenuItems();
} catch (error) {
console.error("Error deleting menu item:", error);
showToast("Failed to delete menu item", "error");
}
}
// Toggle visibility
async function toggleVisibility(id) {
const item = menuItems.find((m) => m.id === id);
if (!item) return;
try {
const response = await fetch(`/api/menu/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ ...item, isvisible: !item.isvisible }),
});
if (!response.ok) throw new Error("Failed to update visibility");
showToast(
`Menu item ${!item.isvisible ? "shown" : "hidden"}`,
"success",
);
loadMenuItems();
} catch (error) {
console.error("Error toggling visibility:", error);
showToast("Failed to update visibility", "error");
}
}
// Reset form
function resetForm() {
document.getElementById("menuItemForm").reset();
document.getElementById("menuItemId").value = "";
selectedIcon = "";
document
.querySelectorAll(".icon-option")
.forEach((opt) => opt.classList.remove("selected"));
document.getElementById("formTitle").innerHTML =
'<i class="bi bi-plus-circle"></i> Add Menu Item';
document.getElementById("submitBtnText").textContent = "Add Menu Item";
document.getElementById("cancelBtn").style.display = "none";
}
// Drag and drop handlers
function handleDragStart(e, index) {
draggedItem = index;
e.target.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}
function handleDragEnter(e) {
e.preventDefault();
const item = e.target.closest(".menu-item");
if (item) item.classList.add("drag-over");
}
function handleDragLeave(e) {
const item = e.target.closest(".menu-item");
if (item) item.classList.remove("drag-over");
}
function handleDrop(e, dropIndex) {
e.preventDefault();
const item = e.target.closest(".menu-item");
if (item) item.classList.remove("drag-over");
if (draggedItem === null || draggedItem === dropIndex) return;
// Reorder array
const [movedItem] = menuItems.splice(draggedItem, 1);
menuItems.splice(dropIndex, 0, movedItem);
// Update display orders
menuItems.forEach((item, index) => {
item.display_order = index;
});
renderMenuItems();
saveOrder();
}
function handleDragEnd(e) {
e.target.classList.remove("dragging");
document.querySelectorAll(".menu-item").forEach((item) => {
item.classList.remove("drag-over");
});
draggedItem = null;
}
// Save order
async function saveOrder() {
try {
const response = await fetch("/api/menu/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(
menuItems.map((item, index) => ({
id: item.id,
display_order: index,
})),
),
});
if (!response.ok) throw new Error("Failed to save order");
showToast("Menu order saved!", "success");
} catch (error) {
console.error("Error saving order:", error);
showToast("Failed to save order", "error");
}
}
// Show toast
function showToast(message, type = "info") {
const container = document.getElementById("toastContainer");
const bgClass =
{
success: "bg-success",
error: "bg-danger",
warning: "bg-warning",
info: "bg-info",
}[type] || "bg-info";
const icon =
{
success: "bi-check-circle",
error: "bi-x-circle",
warning: "bi-exclamation-triangle",
info: "bi-info-circle",
}[type] || "bi-info-circle";
const toast = document.createElement("div");
toast.className = `toast show ${bgClass} text-white`;
toast.innerHTML = `
<div class="d-flex align-items-center p-3">
<i class="bi ${icon} me-2"></i>
<div class="flex-grow-1">${escapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white" onclick="this.parentElement.parentElement.remove()"></button>
</div>
`;
container.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
// Escape HTML
function escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Logout
function logout() {
window.location.href = "/admin/logout";
}
</script>
</body>
</html>