Add core admin dashboard files (HTML, CSS, JS)
This commit is contained in:
parent
889578f180
commit
4d590fd822
9 changed files with 4663 additions and 0 deletions
272
admin-dashboard/product-detail.html
Normal file
272
admin-dashboard/product-detail.html
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Product Detail - KHY</title>
|
||||||
|
<link rel="stylesheet" href="assets/styles/main.css" />
|
||||||
|
<style>
|
||||||
|
/* Force responsive behavior */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.sm\:flex {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&family=Poppins:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white font-sans text-gray-800">
|
||||||
|
<!-- Header -->
|
||||||
|
<header
|
||||||
|
class="fixed w-full h-20 sm:h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
|
||||||
|
>
|
||||||
|
<nav class="h-full">
|
||||||
|
<div
|
||||||
|
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"
|
||||||
|
></div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="pt-20 sm:pt-28">
|
||||||
|
<!-- Product Details Section -->
|
||||||
|
<section class="relative w-full bg-white py-6">
|
||||||
|
<div class="max-w-7xl mx-auto px-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1
|
||||||
|
class="font-playfair font-normal text-base text-quick-silver"
|
||||||
|
id="product-details-title"
|
||||||
|
>
|
||||||
|
Product Details
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main Product Section -->
|
||||||
|
<section class="relative w-full bg-white py-8">
|
||||||
|
<div class="max-w-7xl mx-auto px-5">
|
||||||
|
<div class="flex flex-col md:flex-row gap-8">
|
||||||
|
<!-- Left Section - Product Images -->
|
||||||
|
<div class="flex flex-col-reverse md:flex-row gap-4">
|
||||||
|
<!-- Thumbnails -->
|
||||||
|
<div
|
||||||
|
class="flex flex-row md:flex-col gap-3 md:gap-4 w-full md:w-32 overflow-x-auto md:overflow-visible"
|
||||||
|
>
|
||||||
|
<!-- Thumbnail images will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Product Image -->
|
||||||
|
<div
|
||||||
|
class="w-full h-80 md:w-[500px] md:h-[500px] bg-floral-white rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Main product image will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Section - Product Details -->
|
||||||
|
<div class="w-full md:w-[500px]">
|
||||||
|
<!-- Product Title -->
|
||||||
|
<h1
|
||||||
|
class="font-playfair font-normal text-4xl text-black mb-8"
|
||||||
|
id="product-title"
|
||||||
|
>
|
||||||
|
<!-- Product title will be populated dynamically -->
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Product Description -->
|
||||||
|
<p
|
||||||
|
class="font-playfair font-normal text-sm text-black mb-8 max-w-md"
|
||||||
|
id="product-description"
|
||||||
|
>
|
||||||
|
<!-- Product description will be populated dynamically -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Quantity and Action Buttons -->
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 md:gap-6 mb-3">
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<a
|
||||||
|
href="contact.html"
|
||||||
|
class="inline-flex items-center justify-center w-full md:w-[440px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-black hover:text-white hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap mb-4"
|
||||||
|
>
|
||||||
|
Request a Quote
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
id="compare-products-btn"
|
||||||
|
class="inline-flex items-center justify-center w-full md:w-[440px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-black hover:text-white hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Compare Products
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="w-full h-px bg-light-silver mb-3"></div>
|
||||||
|
|
||||||
|
<!-- Product Specifications -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="space-y-2" id="product-specifications">
|
||||||
|
<!-- Product specifications will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Metadata -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Product metadata will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Product Detail Tabs Section -->
|
||||||
|
<section id="product-tabs" class="w-full bg-white">
|
||||||
|
<div class="border-t border-light-silver"></div>
|
||||||
|
<div class="max-w-7xl mx-auto px-5 py-10">
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex justify-center gap-12 mb-8">
|
||||||
|
<button class="font-playfair text-2xl text-black">
|
||||||
|
Description
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copy -->
|
||||||
|
<div
|
||||||
|
class="max-w-5xl mx-auto space-y-6 text-center"
|
||||||
|
id="product-description-content"
|
||||||
|
>
|
||||||
|
<!-- Product description content will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Images -->
|
||||||
|
<div class="relative mt-10">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||||
|
id="product-gallery-images"
|
||||||
|
>
|
||||||
|
<!-- Product gallery images will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
<!-- Back Arrow -->
|
||||||
|
<button
|
||||||
|
id="gallery-back-btn"
|
||||||
|
class="absolute top-1/2 left-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Forward Arrow -->
|
||||||
|
<button
|
||||||
|
id="gallery-next-btn"
|
||||||
|
class="absolute top-1/2 right-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- full-width divider before related products -->
|
||||||
|
<div class="w-full border-t border-light-silver"></div>
|
||||||
|
|
||||||
|
<!-- Related Products Section -->
|
||||||
|
<section id="related-products" class="w-full bg-white py-14">
|
||||||
|
<div class="max-w-7xl mx-auto px-5">
|
||||||
|
<h2
|
||||||
|
class="font-playfair text-4xl font-medium text-center text-black mb-10"
|
||||||
|
>
|
||||||
|
Related Products
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="related-grid"
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"
|
||||||
|
>
|
||||||
|
<!-- Related items will be injected here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 flex justify-center">
|
||||||
|
<button
|
||||||
|
id="related-show-more"
|
||||||
|
class="w-[245px] h-12 border border-uc-gold rounded-md bg-white font-playfair font-semibold text-base text-uc-gold hover:bg-uc-gold hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Show More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- full-width divider after related products -->
|
||||||
|
<div class="w-full border-t border-light-silver"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Image Enlargement Modal -->
|
||||||
|
<div
|
||||||
|
id="image-modal"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4 transition-opacity duration-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative max-w-7xl max-h-full w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<!-- Close Button -->
|
||||||
|
<button
|
||||||
|
id="modal-close-btn"
|
||||||
|
class="absolute top-4 right-4 z-10 bg-white bg-opacity-20 hover:bg-opacity-30 text-white rounded-full p-3 transition-all duration-200 backdrop-blur-sm"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Enlarged Image -->
|
||||||
|
<img
|
||||||
|
id="modal-image"
|
||||||
|
src=""
|
||||||
|
alt=""
|
||||||
|
class="w-[95vw] h-[95vh] object-contain rounded-lg shadow-2xl transform transition-transform duration-300 scale-95"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="scripts/main.js?v=3.5"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
958
admin-dashboard/src/css/admin.css
Normal file
958
admin-dashboard/src/css/admin.css
Normal file
|
|
@ -0,0 +1,958 @@
|
||||||
|
/* Admin Dashboard Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Montserrat", sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.admin-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left h1 {
|
||||||
|
font-family: "Playfair Display", serif;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left p {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Styles */
|
||||||
|
.admin-nav {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
padding: 15px 25px;
|
||||||
|
border: none;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn-undo {
|
||||||
|
background: #fff3cd !important;
|
||||||
|
color: #856404 !important;
|
||||||
|
border: 1px solid #ffeaa7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn-undo:hover {
|
||||||
|
background: #ffeaa7 !important;
|
||||||
|
color: #6c5b00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.admin-main {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
font-family: "Playfair Display", serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 250px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #6c757d 0%, #adb5bd 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Products Grid */
|
||||||
|
.products-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover .product-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h3 {
|
||||||
|
font-family: "Playfair Display", serif;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-category {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-actions {
|
||||||
|
padding: 15px 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: 1px solid #f8f9fa;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Categories List */
|
||||||
|
.categories-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card h3 {
|
||||||
|
font-family: "Playfair Display", serif;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-id {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image Manager */
|
||||||
|
.image-manager {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item-info {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Container */
|
||||||
|
.preview-container {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 2% auto;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 25px 30px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-family: "Playfair Display", serif;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional Information Section */
|
||||||
|
.additional-info-section {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-section h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-new-field-btn {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-item label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-item input,
|
||||||
|
.additional-info-item textarea {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-item input:focus,
|
||||||
|
.additional-info-item textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-item textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New field creation form */
|
||||||
|
.new-field-form {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 2px dashed #2196f3;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-field-form h5 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #1976d2;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-field-inputs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-field-inputs input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-field-inputs button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove field button */
|
||||||
|
.remove-field-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-field-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image Delete Confirmation Dialog */
|
||||||
|
.image-delete-confirmation {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-content {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-content h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-content p {
|
||||||
|
margin: 10px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-content .warning {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f8d7da;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-buttons .btn {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image Preview */
|
||||||
|
.image-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-item .remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-item .remove-btn:hover {
|
||||||
|
background: rgba(220, 53, 69, 1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sync Status */
|
||||||
|
.sync-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-step.active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-step.completed {
|
||||||
|
background: #e8f5e8;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-step.error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-text {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 5% auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional info responsive */
|
||||||
|
.additional-info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-field-inputs {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success/Error Messages */
|
||||||
|
.message {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Products Modal Styles */
|
||||||
|
.products-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-modal-header p {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-products {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
352
admin-dashboard/src/index.html
Normal file
352
admin-dashboard/src/index.html
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>KHY Admin Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="css/admin.css" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="admin-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="admin-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1>KHY Admin Dashboard</h1>
|
||||||
|
<p>Product Management System</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="preview-btn" class="btn btn-secondary">
|
||||||
|
Preview Site
|
||||||
|
</button>
|
||||||
|
<button id="sync-btn" class="btn btn-primary">
|
||||||
|
Sync to Main Site
|
||||||
|
</button>
|
||||||
|
<button id="quick-sync-btn" class="btn btn-secondary">
|
||||||
|
⚡ Quick Sync
|
||||||
|
</button>
|
||||||
|
<button id="logout-btn" class="btn btn-outline">Logout</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="admin-nav">
|
||||||
|
<div class="nav-left">
|
||||||
|
<button class="nav-btn active" data-section="categories">
|
||||||
|
<span class="nav-icon">🏠</span>
|
||||||
|
Home
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" data-section="products">
|
||||||
|
<span class="nav-icon">📦</span>
|
||||||
|
Products
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" data-section="preview">
|
||||||
|
<span class="nav-icon">👁️</span>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<button class="nav-btn nav-btn-undo" id="undo-changes-btn">
|
||||||
|
<span class="nav-icon">↶</span>
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="admin-main">
|
||||||
|
<!-- Categories Section -->
|
||||||
|
<section id="categories-section" class="content-section active">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Manage your products here</h2>
|
||||||
|
</div>
|
||||||
|
<div class="categories-list" id="categories-list">
|
||||||
|
<!-- Categories will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Products Section -->
|
||||||
|
<section id="products-section" class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Product Management</h2>
|
||||||
|
<div class="section-actions">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="product-search"
|
||||||
|
placeholder="Search products..."
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<button id="add-product-btn" class="btn btn-success">
|
||||||
|
<span>+</span> Add Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="products-grid" id="products-grid">
|
||||||
|
<!-- Products will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Preview Section -->
|
||||||
|
<section id="preview-section" class="content-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Site Preview</h2>
|
||||||
|
<div class="preview-controls">
|
||||||
|
<button id="preview-catalog" class="btn btn-primary">
|
||||||
|
Product Catalog
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-container">
|
||||||
|
<iframe
|
||||||
|
id="preview-frame"
|
||||||
|
src="../preview/catalog.html"
|
||||||
|
frameborder="0"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Category Products Section (Hidden by default) -->
|
||||||
|
<section
|
||||||
|
id="category-products-section"
|
||||||
|
class="content-section"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 id="category-products-title">Category Products</h2>
|
||||||
|
<div class="section-actions">
|
||||||
|
<button class="btn btn-secondary" id="back-to-categories-btn">
|
||||||
|
← Back to Categories
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" id="add-product-to-category-btn">
|
||||||
|
<span>+</span> Add Product to this Category
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="category-products-container">
|
||||||
|
<div class="category-info">
|
||||||
|
<p class="product-count" id="category-product-count">
|
||||||
|
0 products in this category
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="products-list" id="category-products-list">
|
||||||
|
<!-- Products will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Modal -->
|
||||||
|
<div id="product-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modal-title">Add Product</h3>
|
||||||
|
<span class="close">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="product-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-name">Product Name *</label>
|
||||||
|
<input type="text" id="product-name" name="name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-category">Category *</label>
|
||||||
|
<select id="product-category" name="category" required>
|
||||||
|
<option value="">Select Category</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-description">Short Description *</label>
|
||||||
|
<textarea
|
||||||
|
id="product-description"
|
||||||
|
name="description"
|
||||||
|
rows="2"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-description-long">Long Description</label>
|
||||||
|
<textarea
|
||||||
|
id="product-description-long"
|
||||||
|
name="descriptionLong"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Enter each paragraph on a new line"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-material">Material</label>
|
||||||
|
<input type="text" id="product-material" name="material" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-design">Design</label>
|
||||||
|
<input type="text" id="product-design" name="design" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-use-cases">Use Cases</label>
|
||||||
|
<input type="text" id="product-use-cases" name="useCases" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-warranty">Warranty</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="product-warranty"
|
||||||
|
name="warranty"
|
||||||
|
value="5 years"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product-images">Product Images</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="product-images"
|
||||||
|
name="images"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
<div class="image-preview" id="image-preview"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Information Section -->
|
||||||
|
<div
|
||||||
|
class="additional-info-section"
|
||||||
|
id="additional-info-section"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<div class="additional-info-header">
|
||||||
|
<h4>Additional Information</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
id="add-new-field-btn"
|
||||||
|
>
|
||||||
|
+ Add New Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="additional-info-grid" id="additional-info-grid">
|
||||||
|
<!-- Additional info fields will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
id="cancel-product-btn"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Save Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Modal -->
|
||||||
|
<div id="category-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="category-modal-title">Add Category</h3>
|
||||||
|
<span class="close">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="category-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-id">Category ID *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="category-id"
|
||||||
|
name="id"
|
||||||
|
required
|
||||||
|
placeholder="e.g., task-chairs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-name">Category Name *</label>
|
||||||
|
<input type="text" id="category-name" name="name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-description">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="category-description"
|
||||||
|
name="description"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
id="cancel-category-btn"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Save Category
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sync Status Modal -->
|
||||||
|
<div id="sync-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Syncing to Main Site</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="sync-status">
|
||||||
|
<div class="sync-step" id="sync-step-1">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Preparing sync...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-step" id="sync-step-2">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Cloning main repository...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-step" id="sync-step-3">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Syncing products...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-step" id="sync-step-4">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Syncing images...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-step" id="sync-step-5">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Committing changes...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-step" id="sync-step-6">
|
||||||
|
<span class="step-icon">⏳</span>
|
||||||
|
<span class="step-text">Pushing to main site...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <script src="js/storageManager.js"></script> No longer needed - using direct saves -->
|
||||||
|
<script src="js/productManager.js"></script>
|
||||||
|
<script src="js/categoryManager.js"></script>
|
||||||
|
<script src="js/imageManager.js"></script>
|
||||||
|
<script src="js/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
543
admin-dashboard/src/js/admin.js
Normal file
543
admin-dashboard/src/js/admin.js
Normal file
|
|
@ -0,0 +1,543 @@
|
||||||
|
// Main Admin Dashboard Controller
|
||||||
|
class AdminDashboard {
|
||||||
|
constructor() {
|
||||||
|
this.currentSection = "categories";
|
||||||
|
this.productManager = null;
|
||||||
|
this.categoryManager = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.initializeManagers();
|
||||||
|
this.loadInitialData();
|
||||||
|
|
||||||
|
// Debug: Check if modal exists
|
||||||
|
const modal = document.getElementById("product-modal");
|
||||||
|
console.log("Product modal element exists:", !!modal);
|
||||||
|
if (modal) {
|
||||||
|
console.log("Modal current display:", modal.style.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Navigation
|
||||||
|
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
const button = e.currentTarget;
|
||||||
|
this.switchSection(button.dataset.section);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header actions
|
||||||
|
document.getElementById("preview-btn").addEventListener("click", () => {
|
||||||
|
this.switchSection("preview");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("sync-btn").addEventListener("click", () => {
|
||||||
|
this.syncToMainSite();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("quick-sync-btn").addEventListener("click", () => {
|
||||||
|
this.quickSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("logout-btn").addEventListener("click", () => {
|
||||||
|
this.logout();
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("undo-changes-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
this.undoChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preview control buttons
|
||||||
|
const catalogBtn = document.getElementById("preview-catalog");
|
||||||
|
|
||||||
|
if (catalogBtn) {
|
||||||
|
catalogBtn.addEventListener("click", () => {
|
||||||
|
this.loadPreview("catalog");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal close handlers
|
||||||
|
document.querySelectorAll(".close").forEach((closeBtn) => {
|
||||||
|
closeBtn.addEventListener("click", (e) => {
|
||||||
|
const modal = e.target.closest(".modal");
|
||||||
|
modal.style.display = "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.addEventListener("click", (e) => {
|
||||||
|
if (
|
||||||
|
e.target.classList.contains("modal") &&
|
||||||
|
e.target === e.currentTarget
|
||||||
|
) {
|
||||||
|
e.target.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
this.closeAllModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeManagers() {
|
||||||
|
this.productManager = new ProductManager(this);
|
||||||
|
this.categoryManager = new CategoryManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadInitialData() {
|
||||||
|
try {
|
||||||
|
await this.productManager.loadData();
|
||||||
|
await this.categoryManager.loadData();
|
||||||
|
|
||||||
|
// Render categories immediately since Categories is the default active section
|
||||||
|
this.categoryManager.renderCategories();
|
||||||
|
|
||||||
|
this.showMessage("Dashboard loaded successfully", "success");
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage(
|
||||||
|
"Error loading dashboard data: " + error.message,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchSection(sectionName) {
|
||||||
|
// Update navigation
|
||||||
|
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
||||||
|
btn.classList.remove("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update nav button if it's a regular section
|
||||||
|
if (sectionName !== "category-products") {
|
||||||
|
const navBtn = document.querySelector(`[data-section="${sectionName}"]`);
|
||||||
|
if (navBtn) {
|
||||||
|
navBtn.classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update content - hide all sections first
|
||||||
|
document.querySelectorAll(".content-section").forEach((section) => {
|
||||||
|
section.classList.remove("active");
|
||||||
|
section.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the target section
|
||||||
|
const targetSection = document.getElementById(`${sectionName}-section`);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.classList.add("active");
|
||||||
|
targetSection.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSection = sectionName;
|
||||||
|
|
||||||
|
// Load section-specific data
|
||||||
|
this.loadSectionData(sectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSectionData(sectionName) {
|
||||||
|
switch (sectionName) {
|
||||||
|
case "categories":
|
||||||
|
this.categoryManager.renderCategories();
|
||||||
|
break;
|
||||||
|
case "products":
|
||||||
|
this.productManager.renderProducts();
|
||||||
|
break;
|
||||||
|
case "preview":
|
||||||
|
this.loadPreview();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPreview(type = "catalog") {
|
||||||
|
const iframe = document.getElementById("preview-frame");
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
|
||||||
|
if (type === "catalog") {
|
||||||
|
iframe.src = `../preview/catalog.html?t=${timestamp}`;
|
||||||
|
} else if (type === "detail") {
|
||||||
|
iframe.src = `../preview/detail.html?t=${timestamp}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncToMainSite() {
|
||||||
|
const syncModal = document.getElementById("sync-modal");
|
||||||
|
syncModal.style.display = "block";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Copy admin dashboard data to main website
|
||||||
|
if (this.productManager) {
|
||||||
|
await this.copyDataToMainWebsite();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Proceed with main site sync
|
||||||
|
await this.simulateSyncProcess();
|
||||||
|
this.showMessage("Successfully synced to main site!", "success");
|
||||||
|
} catch (error) {
|
||||||
|
this.showMessage("Sync failed: " + error.message, "error");
|
||||||
|
} finally {
|
||||||
|
syncModal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async quickSync() {
|
||||||
|
try {
|
||||||
|
console.log("⚡ QUICK SYNC STARTED");
|
||||||
|
|
||||||
|
// Get the current data from localStorage backup
|
||||||
|
const backupData = localStorage.getItem("khy_products_backup");
|
||||||
|
if (!backupData) {
|
||||||
|
this.showMessage(
|
||||||
|
"No changes to sync. Save a product first!",
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(backupData);
|
||||||
|
console.log("📦 Syncing data:", data.products.length, "products");
|
||||||
|
|
||||||
|
// Create a simple copy command for the user
|
||||||
|
const dataStr = JSON.stringify(data, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.download = "products_sync.json";
|
||||||
|
downloadLink.style.display = "none";
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// Show instructions
|
||||||
|
this.showMessage(
|
||||||
|
"File downloaded! Copy it to data/products.json",
|
||||||
|
"info"
|
||||||
|
);
|
||||||
|
console.log("📋 QUICK SYNC INSTRUCTIONS:");
|
||||||
|
console.log("1. File 'products_sync.json' has been downloaded");
|
||||||
|
console.log("2. Copy it to replace: data/products.json");
|
||||||
|
console.log("3. Refresh the preview to see changes");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Quick sync failed:", error);
|
||||||
|
this.showMessage("Quick sync failed: " + error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyDataToMainWebsite() {
|
||||||
|
try {
|
||||||
|
console.log("🔄 COPYING DATA TO MAIN WEBSITE");
|
||||||
|
|
||||||
|
// Get the current data from localStorage backup
|
||||||
|
const backupData = localStorage.getItem("khy_products_backup");
|
||||||
|
if (!backupData) {
|
||||||
|
console.log("⚠️ No backup data found in localStorage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(backupData);
|
||||||
|
console.log("📦 Data to copy:", data.products.length, "products");
|
||||||
|
|
||||||
|
// Create a simple file copy using Node.js script
|
||||||
|
// This will be handled by the sync-to-main.js script
|
||||||
|
console.log("✅ Data ready for sync - will be processed by sync script");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error preparing data for sync:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncTemporaryChangesToLocal() {
|
||||||
|
try {
|
||||||
|
// Check if there are temporary changes to sync
|
||||||
|
const temporaryChanges =
|
||||||
|
await window.storageManager.getTemporaryChanges();
|
||||||
|
|
||||||
|
if (
|
||||||
|
temporaryChanges.products &&
|
||||||
|
Object.keys(temporaryChanges.products).length > 0
|
||||||
|
) {
|
||||||
|
console.log("🔄 Syncing temporary changes to local files...");
|
||||||
|
|
||||||
|
// Handle image changes first
|
||||||
|
if (temporaryChanges.imageChanges) {
|
||||||
|
console.log("📸 Processing image changes...");
|
||||||
|
await this.processImageChanges(temporaryChanges.imageChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply changes to actual products array
|
||||||
|
Object.values(temporaryChanges.products).forEach((tempProduct) => {
|
||||||
|
const existingIndex = this.productManager.products.findIndex(
|
||||||
|
(p) => p.id === tempProduct.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// Update existing product
|
||||||
|
this.productManager.products[existingIndex] = {
|
||||||
|
...this.productManager.products[existingIndex],
|
||||||
|
...tempProduct,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Add new product
|
||||||
|
this.productManager.products.push(tempProduct);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to actual JSON file
|
||||||
|
await this.productManager.saveData();
|
||||||
|
|
||||||
|
// Clear temporary storage
|
||||||
|
await window.storageManager.clearAllTemporaryData();
|
||||||
|
|
||||||
|
console.log("✅ Temporary changes synced to local files successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error syncing temporary changes:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImageChanges(imageChanges) {
|
||||||
|
console.log("📸 PROCESSING IMAGE CHANGES:");
|
||||||
|
console.log("Image changes:", imageChanges);
|
||||||
|
|
||||||
|
for (const [productId, changes] of Object.entries(imageChanges)) {
|
||||||
|
console.log(`📦 Processing images for product ${productId}:`, changes);
|
||||||
|
|
||||||
|
// Handle new image uploads
|
||||||
|
if (changes.newImages && changes.newImages.length > 0) {
|
||||||
|
console.log("📁 Uploading new images:", changes.newImages);
|
||||||
|
for (const imageRef of changes.newImages) {
|
||||||
|
// Get the actual file from IndexedDB
|
||||||
|
const imageData = await window.storageManager.getImageFile(
|
||||||
|
imageRef.imageId
|
||||||
|
);
|
||||||
|
if (imageData) {
|
||||||
|
await this.uploadImageFile(imageData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deleted images
|
||||||
|
if (changes.deletedImages && changes.deletedImages.length > 0) {
|
||||||
|
console.log("🗑️ Deleting images:", changes.deletedImages);
|
||||||
|
for (const deletedImage of changes.deletedImages) {
|
||||||
|
await this.deleteImageFile(deletedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImageFile(imageData) {
|
||||||
|
console.log("📤 UPLOADING IMAGE FILE:", imageData.name);
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Create FormData with the file
|
||||||
|
// 2. Upload to server/assets directory
|
||||||
|
// 3. Return the new file path
|
||||||
|
// 4. Handle file naming conflicts
|
||||||
|
|
||||||
|
// For now, simulate the upload process
|
||||||
|
const newPath = `assets/images/products/uploaded/${Date.now()}_${
|
||||||
|
imageData.name
|
||||||
|
}`;
|
||||||
|
console.log(`📁 File would be uploaded to: ${newPath}`);
|
||||||
|
console.log(`📁 File size: ${imageData.size} bytes`);
|
||||||
|
console.log(`📁 File type: ${imageData.type}`);
|
||||||
|
|
||||||
|
// TODO: Implement actual file upload
|
||||||
|
// The actual file is in imageData.file
|
||||||
|
return newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteImageFile(deletedImage) {
|
||||||
|
console.log("🗑️ DELETING IMAGE FILE:", deletedImage.path);
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Check if file exists
|
||||||
|
// 2. Delete the file from assets directory
|
||||||
|
// 3. Handle any errors gracefully
|
||||||
|
|
||||||
|
// For now, simulate the deletion process
|
||||||
|
console.log(`🗑️ File would be deleted: ${deletedImage.path}`);
|
||||||
|
|
||||||
|
// TODO: Implement actual file deletion
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async simulateSyncProcess() {
|
||||||
|
const steps = [
|
||||||
|
"sync-step-1",
|
||||||
|
"sync-step-2",
|
||||||
|
"sync-step-3",
|
||||||
|
"sync-step-4",
|
||||||
|
"sync-step-5",
|
||||||
|
"sync-step-6",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
const step = document.getElementById(steps[i]);
|
||||||
|
step.classList.add("active");
|
||||||
|
|
||||||
|
// Simulate processing time
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
step.classList.remove("active");
|
||||||
|
step.classList.add("completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real implementation, this would call the actual sync script
|
||||||
|
console.log("Sync process completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
if (confirm("Are you sure you want to logout?")) {
|
||||||
|
// In a real implementation, this would clear authentication
|
||||||
|
window.location.href = "login.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undoChanges() {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to undo all unsaved changes? This action cannot be undone."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Call the productManager's undo method
|
||||||
|
if (this.productManager) {
|
||||||
|
this.productManager.undoTemporaryChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllModals() {
|
||||||
|
document.querySelectorAll(".modal").forEach((modal) => {
|
||||||
|
modal.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(message, type = "info") {
|
||||||
|
// Remove existing messages
|
||||||
|
const existingMessages = document.querySelectorAll(".message");
|
||||||
|
existingMessages.forEach((msg) => msg.remove());
|
||||||
|
|
||||||
|
// Create new message
|
||||||
|
const messageDiv = document.createElement("div");
|
||||||
|
messageDiv.className = `message ${type}`;
|
||||||
|
messageDiv.textContent = message;
|
||||||
|
|
||||||
|
// Insert at the top of the main content
|
||||||
|
const main = document.querySelector(".admin-main");
|
||||||
|
main.insertBefore(messageDiv, main.firstChild);
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
messageDiv.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method for other managers to use
|
||||||
|
getData() {
|
||||||
|
return {
|
||||||
|
products: this.productManager.products,
|
||||||
|
categories: this.categoryManager.categories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to refresh current section
|
||||||
|
refreshCurrentSection() {
|
||||||
|
this.loadSectionData(this.currentSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
async undoChanges() {
|
||||||
|
const undoStack = JSON.parse(
|
||||||
|
localStorage.getItem("khy_undo_stack") || "[]"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (undoStack.length === 0) {
|
||||||
|
this.showMessage("No changes to undo.", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastChange = undoStack[undoStack.length - 1];
|
||||||
|
const confirmMessage = `Undo last change: ${lastChange.action.toUpperCase()} "${
|
||||||
|
lastChange.newData.name
|
||||||
|
}"?\n\nThis action cannot be undone.`;
|
||||||
|
|
||||||
|
if (confirm(confirmMessage)) {
|
||||||
|
try {
|
||||||
|
await this.performUndo(lastChange);
|
||||||
|
|
||||||
|
// Remove the undone change from the stack
|
||||||
|
undoStack.pop();
|
||||||
|
localStorage.setItem("khy_undo_stack", JSON.stringify(undoStack));
|
||||||
|
|
||||||
|
this.showMessage(
|
||||||
|
`Successfully undid: ${lastChange.action} "${lastChange.newData.name}"`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the current view
|
||||||
|
this.productManager.refreshCurrentView();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ ERROR DURING UNDO:", error);
|
||||||
|
this.showMessage("Error during undo: " + error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performUndo(changeEntry) {
|
||||||
|
console.log("⏪ PERFORMING UNDO:", changeEntry);
|
||||||
|
|
||||||
|
if (changeEntry.action === "edit") {
|
||||||
|
// Restore the original product data
|
||||||
|
const index = this.productManager.products.findIndex(
|
||||||
|
(p) => p.id === changeEntry.productId
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.productManager.products[index] = changeEntry.originalData;
|
||||||
|
console.log("✅ Restored original product data");
|
||||||
|
}
|
||||||
|
} else if (changeEntry.action === "add") {
|
||||||
|
// Remove the added product
|
||||||
|
const index = this.productManager.products.findIndex(
|
||||||
|
(p) => p.id === changeEntry.productId
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.productManager.products.splice(index, 1);
|
||||||
|
console.log("✅ Removed added product");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the changes
|
||||||
|
await this.productManager.saveData();
|
||||||
|
console.log("💾 Undo changes saved to file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize dashboard when DOM is loaded
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.adminDashboard = new AdminDashboard();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global utility functions
|
||||||
|
window.showMessage = (message, type) => {
|
||||||
|
if (window.adminDashboard) {
|
||||||
|
window.adminDashboard.showMessage(message, type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.refreshDashboard = () => {
|
||||||
|
if (window.adminDashboard) {
|
||||||
|
window.adminDashboard.refreshCurrentSection();
|
||||||
|
}
|
||||||
|
};
|
||||||
459
admin-dashboard/src/js/categoryManager.js
Normal file
459
admin-dashboard/src/js/categoryManager.js
Normal file
|
|
@ -0,0 +1,459 @@
|
||||||
|
// Category Management Class
|
||||||
|
class CategoryManager {
|
||||||
|
constructor(dashboard) {
|
||||||
|
this.dashboard = dashboard;
|
||||||
|
this.categories = [];
|
||||||
|
this.currentCategory = null;
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Add category button - removed from UI
|
||||||
|
// document
|
||||||
|
// .getElementById("add-category-btn")
|
||||||
|
// .addEventListener("click", () => {
|
||||||
|
// this.showCategoryModal();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Category form submit
|
||||||
|
document.getElementById("category-form").addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveCategory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel button in category modal
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.id === "cancel-category-btn") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.hideCategoryModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// View Products buttons - use event delegation
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.matches("[data-category-id]")) {
|
||||||
|
const categoryId = e.target.getAttribute("data-category-id");
|
||||||
|
this.viewCategoryProducts(categoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Back to Categories button
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.id === "back-to-categories-btn") {
|
||||||
|
this.backToHome();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add Product to Category button
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.id === "add-product-to-category-btn") {
|
||||||
|
const categoryId = e.target.getAttribute("data-category-id");
|
||||||
|
this.addProductToCategory(categoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit Product buttons in category view - use event delegation
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
console.log("Click detected on:", e.target);
|
||||||
|
// Check if the clicked element or its parent has the edit-product action
|
||||||
|
const editButton = e.target.closest("[data-action='edit-product']");
|
||||||
|
if (editButton) {
|
||||||
|
console.log("Edit product button clicked!");
|
||||||
|
e.stopPropagation();
|
||||||
|
const productId = editButton.getAttribute("data-product-id");
|
||||||
|
console.log("Product ID:", productId);
|
||||||
|
this.editProduct(productId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("../data/products.json");
|
||||||
|
const data = await response.json();
|
||||||
|
this.categories = data.categories || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading category data:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCategories() {
|
||||||
|
const container = document.getElementById("categories-list");
|
||||||
|
|
||||||
|
if (this.categories.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="no-categories">
|
||||||
|
<h3>No categories found</h3>
|
||||||
|
<p>Add your first category to get started!</p>
|
||||||
|
<button class="btn btn-primary" onclick="categoryManager.showCategoryModal()">Add Category</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
this.categories.forEach((category) => {
|
||||||
|
const categoryCard = this.createCategoryCard(category);
|
||||||
|
container.appendChild(categoryCard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategoryCard(category) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "category-card";
|
||||||
|
|
||||||
|
// Count products in this category
|
||||||
|
const productCount = this.dashboard.productManager.products.filter(
|
||||||
|
(p) => p.category === category.id
|
||||||
|
).length;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="category-header">
|
||||||
|
<h3>${category.name}</h3>
|
||||||
|
<span class="category-id">${category.id}</span>
|
||||||
|
</div>
|
||||||
|
<p class="category-description">${
|
||||||
|
category.description || "No description"
|
||||||
|
}</p>
|
||||||
|
<div class="category-stats">
|
||||||
|
<span class="product-count">${productCount} product${
|
||||||
|
productCount !== 1 ? "s" : ""
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" data-category-id="${
|
||||||
|
category.id
|
||||||
|
}">
|
||||||
|
View Products
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
showCategoryModal(category = null) {
|
||||||
|
const modal = document.getElementById("category-modal");
|
||||||
|
const title = document.getElementById("category-modal-title");
|
||||||
|
const form = document.getElementById("category-form");
|
||||||
|
|
||||||
|
this.currentCategory = category;
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
title.textContent = "Edit Category";
|
||||||
|
this.populateForm(category);
|
||||||
|
} else {
|
||||||
|
title.textContent = "Add Category";
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCategoryModal() {
|
||||||
|
document.getElementById("category-modal").style.display = "none";
|
||||||
|
this.currentCategory = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateForm(category) {
|
||||||
|
document.getElementById("category-id").value = category.id;
|
||||||
|
document.getElementById("category-name").value = category.name;
|
||||||
|
document.getElementById("category-description").value =
|
||||||
|
category.description || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCategory() {
|
||||||
|
const formData = new FormData(document.getElementById("category-form"));
|
||||||
|
const categoryData = {
|
||||||
|
id: formData.get("id"),
|
||||||
|
name: formData.get("name"),
|
||||||
|
description: formData.get("description"),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
if (!categoryData.id || !categoryData.name) {
|
||||||
|
throw new Error("Category ID and name are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate ID (only for new categories)
|
||||||
|
if (!this.currentCategory) {
|
||||||
|
const existingCategory = this.categories.find(
|
||||||
|
(cat) => cat.id === categoryData.id
|
||||||
|
);
|
||||||
|
if (existingCategory) {
|
||||||
|
throw new Error("A category with this ID already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or add category
|
||||||
|
const existingIndex = this.categories.findIndex(
|
||||||
|
(cat) => cat.id === categoryData.id
|
||||||
|
);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
this.categories[existingIndex] = {
|
||||||
|
...this.categories[existingIndex],
|
||||||
|
...categoryData,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.categories.push(categoryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveData();
|
||||||
|
this.renderCategories();
|
||||||
|
this.hideCategoryModal();
|
||||||
|
this.dashboard.showMessage("Category saved successfully!", "success");
|
||||||
|
|
||||||
|
// Update product manager's category select
|
||||||
|
if (this.dashboard.productManager) {
|
||||||
|
this.dashboard.productManager.populateCategorySelect();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
"Error saving category: " + error.message,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editCategory(id) {
|
||||||
|
const category = this.categories.find((cat) => cat.id === id);
|
||||||
|
if (category) {
|
||||||
|
this.showCategoryModal(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewCategoryProducts(categoryId) {
|
||||||
|
const category = this.categories.find((cat) => cat.id === categoryId);
|
||||||
|
if (!category) return;
|
||||||
|
|
||||||
|
// Get products in this category
|
||||||
|
const productsInCategory = this.dashboard.productManager.products.filter(
|
||||||
|
(p) => p.category === categoryId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch to the dedicated category products section
|
||||||
|
this.dashboard.switchSection("category-products");
|
||||||
|
|
||||||
|
// Render the category products
|
||||||
|
this.showCategoryProductsScreen(category, productsInCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
showCategoryProductsScreen(category, products) {
|
||||||
|
console.log("showCategoryProductsScreen called with category:", category);
|
||||||
|
console.log("Products in category:", products);
|
||||||
|
|
||||||
|
// Update the section title
|
||||||
|
const title = document.getElementById("category-products-title");
|
||||||
|
if (title) {
|
||||||
|
title.textContent = `${category.name} Products`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the product count
|
||||||
|
const productCount = document.getElementById("category-product-count");
|
||||||
|
if (productCount) {
|
||||||
|
productCount.textContent = `${products.length} product${
|
||||||
|
products.length !== 1 ? "s" : ""
|
||||||
|
} in this category`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the add product button
|
||||||
|
const addButton = document.getElementById("add-product-to-category-btn");
|
||||||
|
if (addButton) {
|
||||||
|
addButton.setAttribute("data-category-id", category.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the products list
|
||||||
|
const productsList = document.getElementById("category-products-list");
|
||||||
|
if (productsList) {
|
||||||
|
productsList.innerHTML =
|
||||||
|
products.length === 0
|
||||||
|
? '<p class="no-products">No products in this category yet. Click "Add Product to this Category" to get started!</p>'
|
||||||
|
: products
|
||||||
|
.map((product) => {
|
||||||
|
console.log(
|
||||||
|
"Rendering product:",
|
||||||
|
product.name,
|
||||||
|
"with ID:",
|
||||||
|
product.id
|
||||||
|
);
|
||||||
|
return `
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-info">
|
||||||
|
<h4>${product.name}</h4>
|
||||||
|
<p class="product-description">${
|
||||||
|
product.description || "No description"
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
<div class="product-actions">
|
||||||
|
<button class="btn btn-sm btn-primary" data-action="edit-product" data-product-id="${
|
||||||
|
product.id
|
||||||
|
}">
|
||||||
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backToHome() {
|
||||||
|
// Switch back to home section
|
||||||
|
this.dashboard.switchSection("categories");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupModalEventListeners() {
|
||||||
|
const modal = document.getElementById("products-modal");
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (
|
||||||
|
e.target.matches("[data-action='close-modal']") ||
|
||||||
|
e.target.classList.contains("modal")
|
||||||
|
) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add product to category
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target.matches("[data-action='add-product']")) {
|
||||||
|
const categoryId = e.target.getAttribute("data-category-id");
|
||||||
|
this.addProductToCategory(categoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// View product details
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target.matches("[data-action='view-product']")) {
|
||||||
|
const productId = e.target.getAttribute("data-product-id");
|
||||||
|
this.viewProductDetails(productId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit product
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target.matches("[data-action='edit-product']")) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const productId = e.target.getAttribute("data-product-id");
|
||||||
|
this.editProduct(productId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addProductToCategory(categoryId) {
|
||||||
|
// Show the product modal directly without switching sections
|
||||||
|
this.dashboard.productManager.showProductModal();
|
||||||
|
|
||||||
|
// Pre-select the category in the form after the modal is shown
|
||||||
|
setTimeout(() => {
|
||||||
|
const categorySelect = document.getElementById("product-category");
|
||||||
|
if (categorySelect) {
|
||||||
|
categorySelect.value = categoryId;
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewProductDetails(productId) {
|
||||||
|
// Close the products modal immediately
|
||||||
|
const modal = document.getElementById("products-modal");
|
||||||
|
if (modal) {
|
||||||
|
modal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to products section and show product details
|
||||||
|
this.dashboard.switchSection("products");
|
||||||
|
// You could implement a product details view here
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
`Viewing product details for ID: ${productId}`,
|
||||||
|
"info"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
editProduct(productId) {
|
||||||
|
console.log("editProduct called with ID:", productId);
|
||||||
|
console.log("Dashboard productManager:", this.dashboard.productManager);
|
||||||
|
// Edit the product directly (we're already in the products section)
|
||||||
|
this.dashboard.productManager.editProduct(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCategory(id) {
|
||||||
|
const category = this.categories.find((cat) => cat.id === id);
|
||||||
|
if (!category) return;
|
||||||
|
|
||||||
|
// Check if category has products
|
||||||
|
const productsInCategory = this.dashboard.productManager.products.filter(
|
||||||
|
(p) => p.category === id
|
||||||
|
);
|
||||||
|
if (productsInCategory.length > 0) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
`Cannot delete category "${category.name}" because it contains ${productsInCategory.length} product(s). Please move or delete the products first.`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Are you sure you want to delete the category "${category.name}"? This action cannot be undone.`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.categories = this.categories.filter((cat) => cat.id !== id);
|
||||||
|
await this.saveData();
|
||||||
|
this.renderCategories();
|
||||||
|
this.dashboard.showMessage("Category deleted successfully!", "success");
|
||||||
|
|
||||||
|
// Update product manager's category select
|
||||||
|
if (this.dashboard.productManager) {
|
||||||
|
this.dashboard.productManager.populateCategorySelect();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
"Error deleting category: " + error.message,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveData() {
|
||||||
|
const data = {
|
||||||
|
products: this.dashboard.productManager.products,
|
||||||
|
categories: this.categories,
|
||||||
|
};
|
||||||
|
|
||||||
|
// In a real implementation, this would save to a server
|
||||||
|
console.log("Saving category data:", data);
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to get category by ID
|
||||||
|
getCategoryById(id) {
|
||||||
|
return this.categories.find((cat) => cat.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to get category name by ID
|
||||||
|
getCategoryName(id) {
|
||||||
|
const category = this.getCategoryById(id);
|
||||||
|
return category ? category.name : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to validate category ID format
|
||||||
|
validateCategoryId(id) {
|
||||||
|
// Category IDs should be lowercase, hyphen-separated
|
||||||
|
return /^[a-z0-9-]+$/.test(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make CategoryManager available globally
|
||||||
|
window.CategoryManager = CategoryManager;
|
||||||
338
admin-dashboard/src/js/imageManager.js
Normal file
338
admin-dashboard/src/js/imageManager.js
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
// Image Management Class
|
||||||
|
class ImageManager {
|
||||||
|
constructor(dashboard) {
|
||||||
|
this.dashboard = dashboard;
|
||||||
|
this.images = [];
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Upload images button
|
||||||
|
document
|
||||||
|
.getElementById("upload-images-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
this.showUploadModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
try {
|
||||||
|
// In a real implementation, this would fetch from a server
|
||||||
|
// For now, we'll collect images from products
|
||||||
|
this.collectImagesFromProducts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading image data:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectImagesFromProducts() {
|
||||||
|
this.images = [];
|
||||||
|
const products = this.dashboard.productManager.products;
|
||||||
|
|
||||||
|
products.forEach((product) => {
|
||||||
|
if (product.images && Array.isArray(product.images)) {
|
||||||
|
product.images.forEach((imagePath) => {
|
||||||
|
if (!this.images.find((img) => img.path === imagePath)) {
|
||||||
|
this.images.push({
|
||||||
|
path: imagePath,
|
||||||
|
name: this.extractFileName(imagePath),
|
||||||
|
productId: product.id,
|
||||||
|
productName: product.name,
|
||||||
|
size: "Unknown", // Would be fetched from server
|
||||||
|
uploadDate: "Unknown", // Would be fetched from server
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extractFileName(path) {
|
||||||
|
return path.split("/").pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderImages() {
|
||||||
|
const container = document.getElementById("image-manager");
|
||||||
|
|
||||||
|
if (this.images.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="no-images">
|
||||||
|
<h3>No images found</h3>
|
||||||
|
<p>Upload images to get started!</p>
|
||||||
|
<button class="btn btn-primary" onclick="imageManager.showUploadModal()">Upload Images</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
this.images.forEach((image) => {
|
||||||
|
const imageItem = this.createImageItem(image);
|
||||||
|
container.appendChild(imageItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createImageItem(image) {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "image-item";
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="image-preview">
|
||||||
|
<img src="${image.path}" alt="${image.name}" onerror="this.src='data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"150\" viewBox=\"0 0 200 150\"><rect width=\"200\" height=\"150\" fill=\"%23f0f0f0\"/><text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" fill=\"%23999\">Image Not Found</text></svg>'">
|
||||||
|
</div>
|
||||||
|
<div class="image-item-info">
|
||||||
|
<div class="image-item-name" title="${image.name}">${image.name}</div>
|
||||||
|
<div class="image-meta">
|
||||||
|
<span class="image-product">Product: ${image.productName}</span>
|
||||||
|
<span class="image-size">Size: ${image.size}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item-actions">
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="imageManager.viewImage('${image.path}')">
|
||||||
|
👁️ View
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="imageManager.renameImage('${image.path}')">
|
||||||
|
✏️ Rename
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="imageManager.deleteImage('${image.path}')">
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
showUploadModal() {
|
||||||
|
// Create upload modal if it doesn't exist
|
||||||
|
let uploadModal = document.getElementById("upload-modal");
|
||||||
|
if (!uploadModal) {
|
||||||
|
uploadModal = this.createUploadModal();
|
||||||
|
document.body.appendChild(uploadModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadModal.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
createUploadModal() {
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.id = "upload-modal";
|
||||||
|
modal.className = "modal";
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Upload Images</h3>
|
||||||
|
<span class="close" onclick="this.closest('.modal').style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="upload-area" id="upload-area">
|
||||||
|
<div class="upload-content">
|
||||||
|
<div class="upload-icon">📤</div>
|
||||||
|
<h4>Drag & Drop images here</h4>
|
||||||
|
<p>or click to select files</p>
|
||||||
|
<input type="file" id="file-input" multiple accept="image/*" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-preview" id="upload-preview"></div>
|
||||||
|
<div class="upload-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="this.closest('.modal').style.display='none'">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="imageManager.processUpload()">Upload Images</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Setup drag and drop
|
||||||
|
const uploadArea = modal.querySelector("#upload-area");
|
||||||
|
const fileInput = modal.querySelector("#file-input");
|
||||||
|
|
||||||
|
uploadArea.addEventListener("click", () => fileInput.click());
|
||||||
|
uploadArea.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.classList.add("drag-over");
|
||||||
|
});
|
||||||
|
uploadArea.addEventListener("dragleave", () => {
|
||||||
|
uploadArea.classList.remove("drag-over");
|
||||||
|
});
|
||||||
|
uploadArea.addEventListener("drop", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.classList.remove("drag-over");
|
||||||
|
this.handleFileSelect(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", (e) => {
|
||||||
|
this.handleFileSelect(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect(files) {
|
||||||
|
const preview = document.getElementById("upload-preview");
|
||||||
|
preview.innerHTML = "";
|
||||||
|
|
||||||
|
Array.from(files).forEach((file) => {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const imageDiv = document.createElement("div");
|
||||||
|
imageDiv.className = "upload-preview-item";
|
||||||
|
imageDiv.innerHTML = `
|
||||||
|
<img src="${e.target.result}" alt="${file.name}">
|
||||||
|
<div class="upload-preview-info">
|
||||||
|
<span class="upload-filename">${file.name}</span>
|
||||||
|
<span class="upload-filesize">${this.formatFileSize(
|
||||||
|
file.size
|
||||||
|
)}</span>
|
||||||
|
</div>
|
||||||
|
<button class="remove-upload-btn" onclick="this.parentElement.remove()">×</button>
|
||||||
|
`;
|
||||||
|
preview.appendChild(imageDiv);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
async processUpload() {
|
||||||
|
const fileInput = document.getElementById("file-input");
|
||||||
|
const files = fileInput.files;
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
this.dashboard.showMessage("Please select files to upload", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.dashboard.showMessage("Uploading images...", "info");
|
||||||
|
|
||||||
|
const uploadedImages = await this.uploadImages(files);
|
||||||
|
|
||||||
|
// Add to images list
|
||||||
|
uploadedImages.forEach((imagePath) => {
|
||||||
|
const fileName = this.extractFileName(imagePath);
|
||||||
|
this.images.push({
|
||||||
|
path: imagePath,
|
||||||
|
name: fileName,
|
||||||
|
productId: null,
|
||||||
|
productName: "Unassigned",
|
||||||
|
size: "Unknown",
|
||||||
|
uploadDate: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderImages();
|
||||||
|
document.getElementById("upload-modal").style.display = "none";
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
`Successfully uploaded ${uploadedImages.length} image(s)!`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
"Error uploading images: " + error.message,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImages(files) {
|
||||||
|
const uploadedImages = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
// In a real implementation, you'd upload to a server
|
||||||
|
const filename = `uploaded_${Date.now()}_${file.name}`;
|
||||||
|
const imagePath = `assets/images/products/${filename}`;
|
||||||
|
uploadedImages.push(imagePath);
|
||||||
|
|
||||||
|
console.log(`Would upload: ${file.name} to ${imagePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadedImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewImage(imagePath) {
|
||||||
|
// Create image viewer modal
|
||||||
|
const viewer = document.createElement("div");
|
||||||
|
viewer.className = "modal image-viewer";
|
||||||
|
viewer.innerHTML = `
|
||||||
|
<div class="modal-content image-viewer-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Image Viewer</h3>
|
||||||
|
<span class="close" onclick="this.closest('.modal').remove()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<img src="${imagePath}" alt="Full size image" style="max-width: 100%; max-height: 70vh; object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(viewer);
|
||||||
|
viewer.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
renameImage(imagePath) {
|
||||||
|
const currentName = this.extractFileName(imagePath);
|
||||||
|
const newName = prompt("Enter new name for the image:", currentName);
|
||||||
|
|
||||||
|
if (newName && newName !== currentName) {
|
||||||
|
// In a real implementation, you'd rename the file on the server
|
||||||
|
console.log(`Would rename ${imagePath} to ${newName}`);
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
"Image rename functionality would be implemented here",
|
||||||
|
"info"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteImage(imagePath) {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to delete this image? This action cannot be undone."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Check if image is used by any products
|
||||||
|
const productsUsingImage =
|
||||||
|
this.dashboard.productManager.products.filter(
|
||||||
|
(product) => product.images && product.images.includes(imagePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (productsUsingImage.length > 0) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
`Cannot delete image because it's used by ${productsUsingImage.length} product(s). Please remove it from the products first.`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from images list
|
||||||
|
this.images = this.images.filter((img) => img.path !== imagePath);
|
||||||
|
|
||||||
|
// In a real implementation, you'd delete from server
|
||||||
|
console.log(`Would delete image: ${imagePath}`);
|
||||||
|
|
||||||
|
this.renderImages();
|
||||||
|
this.dashboard.showMessage("Image deleted successfully!", "success");
|
||||||
|
} catch (error) {
|
||||||
|
this.dashboard.showMessage(
|
||||||
|
"Error deleting image: " + error.message,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make ImageManager available globally
|
||||||
|
window.ImageManager = ImageManager;
|
||||||
1490
admin-dashboard/src/js/productManager.js
Normal file
1490
admin-dashboard/src/js/productManager.js
Normal file
File diff suppressed because it is too large
Load diff
245
admin-dashboard/src/js/storageManager.js
Normal file
245
admin-dashboard/src/js/storageManager.js
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
class StorageManager {
|
||||||
|
constructor() {
|
||||||
|
this.dbName = "KHYAdminStorage";
|
||||||
|
this.dbVersion = 1;
|
||||||
|
this.db = null;
|
||||||
|
this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to open IndexedDB:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
this.db = request.result;
|
||||||
|
console.log("✅ IndexedDB initialized successfully");
|
||||||
|
resolve(this.db);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
|
||||||
|
// Create stores for different types of data
|
||||||
|
if (!db.objectStoreNames.contains("temporaryChanges")) {
|
||||||
|
db.createObjectStore("temporaryChanges", { keyPath: "id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db.objectStoreNames.contains("imageFiles")) {
|
||||||
|
db.createObjectStore("imageFiles", { keyPath: "id" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ IndexedDB stores created");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTemporaryChanges(changes) {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(
|
||||||
|
["temporaryChanges"],
|
||||||
|
"readwrite"
|
||||||
|
);
|
||||||
|
const store = transaction.objectStore("temporaryChanges");
|
||||||
|
|
||||||
|
const changeData = {
|
||||||
|
id: "current",
|
||||||
|
data: changes,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.put(changeData);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log("💾 Temporary changes saved to IndexedDB");
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to save temporary changes:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTemporaryChanges() {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(["temporaryChanges"], "readonly");
|
||||||
|
const store = transaction.objectStore("temporaryChanges");
|
||||||
|
const request = store.get("current");
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const result = request.result;
|
||||||
|
if (result && result.data) {
|
||||||
|
console.log("📦 Retrieved temporary changes from IndexedDB");
|
||||||
|
resolve(result.data);
|
||||||
|
} else {
|
||||||
|
console.log("📦 No temporary changes found in IndexedDB");
|
||||||
|
resolve({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to get temporary changes:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveImageFile(imageId, file) {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
||||||
|
const store = transaction.objectStore("imageFiles");
|
||||||
|
|
||||||
|
const imageData = {
|
||||||
|
id: imageId,
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
lastModified: file.lastModified,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.put(imageData);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log(`📁 Image file saved to IndexedDB: ${file.name}`);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to save image file:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImageFile(imageId) {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
||||||
|
const store = transaction.objectStore("imageFiles");
|
||||||
|
const request = store.get(imageId);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const result = request.result;
|
||||||
|
if (result) {
|
||||||
|
console.log(`📁 Retrieved image file from IndexedDB: ${result.name}`);
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to get image file:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteImageFile(imageId) {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
||||||
|
const store = transaction.objectStore("imageFiles");
|
||||||
|
const request = store.delete(imageId);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log(`🗑️ Image file deleted from IndexedDB: ${imageId}`);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to delete image file:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllTemporaryData() {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(
|
||||||
|
["temporaryChanges", "imageFiles"],
|
||||||
|
"readwrite"
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeStore = transaction.objectStore("temporaryChanges");
|
||||||
|
const imageStore = transaction.objectStore("imageFiles");
|
||||||
|
|
||||||
|
const changeRequest = changeStore.clear();
|
||||||
|
const imageRequest = imageStore.clear();
|
||||||
|
|
||||||
|
let completed = 0;
|
||||||
|
const total = 2;
|
||||||
|
|
||||||
|
const checkComplete = () => {
|
||||||
|
completed++;
|
||||||
|
if (completed === total) {
|
||||||
|
console.log("🧹 All temporary data cleared from IndexedDB");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
changeRequest.onsuccess = checkComplete;
|
||||||
|
imageRequest.onsuccess = checkComplete;
|
||||||
|
|
||||||
|
changeRequest.onerror = () => reject(changeRequest.error);
|
||||||
|
imageRequest.onerror = () => reject(imageRequest.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllImageFiles() {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
||||||
|
const store = transaction.objectStore("imageFiles");
|
||||||
|
const request = store.getAll();
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
console.log(
|
||||||
|
`📁 Retrieved ${request.result.length} image files from IndexedDB`
|
||||||
|
);
|
||||||
|
resolve(request.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.error("Failed to get all image files:", request.error);
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
window.storageManager = new StorageManager();
|
||||||
6
admin-dashboard/start.sh
Executable file
6
admin-dashboard/start.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
echo "🚀 Starting KHY Admin Dashboard..."
|
||||||
|
echo "📱 Dashboard will be available at: http://localhost:3000"
|
||||||
|
echo "🔧 To stop the server, press Ctrl+C"
|
||||||
|
echo ""
|
||||||
|
python3 -m http.server 3000
|
||||||
Loading…
Add table
Add a link
Reference in a new issue