diff --git a/public/admin.html b/public/admin.html
index b2b296f..75b8d65 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -52,6 +52,31 @@
box-sizing: border-box;
}
+ /* Section Loader */
+ .section-loader {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ gap: 16px;
+ }
+ .section-loader .loader-spinner {
+ width: 48px;
+ height: 48px;
+ border: 3px solid var(--card-border);
+ border-top-color: var(--primary);
+ border-radius: 50%;
+ animation: loader-spin 0.8s linear infinite;
+ }
+ .section-loader p {
+ color: var(--text-muted);
+ font-size: 14px;
+ }
+ @keyframes loader-spin {
+ to { transform: rotate(360deg); }
+ }
+
html, body {
height: 100%;
}
@@ -1285,7 +1310,10 @@
diff --git a/public/js/admin.js b/public/js/admin.js
index 84c096f..2abbdcc 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -66,25 +66,50 @@ class AdminPanel {
})
}
+ getLoader() {
+ return document.getElementById('section-loader')
+ }
+
+ showLoader(show) {
+ const loader = this.getLoader()
+ if (loader) loader.style.display = show ? 'flex' : 'none'
+ }
+
async loadSection(section) {
const content = document.getElementById('admin-content')
if (!content) return
+ // Abort any ongoing fetch to prevent race conditions
+ if (this._abortController) this._abortController.abort()
+ this._abortController = new AbortController()
+
// Hide all sections
content.querySelectorAll('.page-section').forEach(s => s.classList.remove('active'))
// If section not loaded yet, fetch it
let sec = document.getElementById(`section-${section}`)
if (!sec) {
+ this.showLoader(true)
try {
- const res = await fetch(`/admin/${section}.html`)
+ const res = await fetch(`/admin/${section}.html`, {
+ signal: this._abortController.signal,
+ credentials: 'same-origin'
+ })
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
const html = await res.text()
+ if (!html || html.length < 50) throw new Error('Empty response')
content.insertAdjacentHTML('beforeend', html)
sec = document.getElementById(`section-${section}`)
- if (!sec) return
+ if (!sec) throw new Error('Section not found in response')
} catch (e) {
- console.error(`Failed to load section: ${section}`, e)
+ if (e.name === 'AbortError') {
+ console.warn(`Section ${section} load aborted`)
+ } else {
+ console.error(`Failed to load section: ${section}`, e)
+ }
return
+ } finally {
+ this.showLoader(false)
}
}
sec.classList.add('active')
diff --git a/src/server/index.ts b/src/server/index.ts
index 362bace..24d380f 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1794,29 +1794,29 @@ const adminHtmlAuthDisabled = async (c: any, next: any) => {
}
// Serve static files and SPA routes (clean URLs without .html)
-// Admin component files - serve explicitly BEFORE the /admin route
-app.get('/admin/sidebar.html', serveStatic({ path: './public/admin/sidebar.html' }))
-app.get('/admin/topbar.html', serveStatic({ path: './public/admin/topbar.html' }))
-app.get('/admin/dashboard.html', serveStatic({ path: './public/admin/dashboard.html' }))
-app.get('/admin/properties.html', serveStatic({ path: './public/admin/properties.html' }))
-app.get('/admin/leads.html', serveStatic({ path: './public/admin/leads.html' }))
-app.get('/admin/testimonials.html', serveStatic({ path: './public/admin/testimonials.html' }))
-app.get('/admin/faq.html', serveStatic({ path: './public/admin/faq.html' }))
-app.get('/admin/services.html', serveStatic({ path: './public/admin/services.html' }))
-app.get('/admin/settings.html', serveStatic({ path: './public/admin/settings.html' }))
-app.get('/admin/users.html', serveStatic({ path: './public/admin/users.html' }))
-app.get('/admin/analytics.html', serveStatic({ path: './public/admin/analytics.html' }))
-app.get('/admin/traffic.html', serveStatic({ path: './public/admin/traffic.html' }))
+// Admin component files - using Bun.file() directly to avoid serveStatic content-length bug
+app.get('/admin/sidebar.html', async (c) => new Response(Bun.file('./public/admin/sidebar.html')))
+app.get('/admin/topbar.html', async (c) => new Response(Bun.file('./public/admin/topbar.html')))
+app.get('/admin/dashboard.html', async (c) => new Response(Bun.file('./public/admin/dashboard.html')))
+app.get('/admin/properties.html', async (c) => new Response(Bun.file('./public/admin/properties.html')))
+app.get('/admin/leads.html', async (c) => new Response(Bun.file('./public/admin/leads.html')))
+app.get('/admin/testimonials.html', async (c) => new Response(Bun.file('./public/admin/testimonials.html')))
+app.get('/admin/faq.html', async (c) => new Response(Bun.file('./public/admin/faq.html')))
+app.get('/admin/services.html', async (c) => new Response(Bun.file('./public/admin/services.html')))
+app.get('/admin/settings.html', async (c) => new Response(Bun.file('./public/admin/settings.html')))
+app.get('/admin/users.html', async (c) => new Response(Bun.file('./public/admin/users.html')))
+app.get('/admin/analytics.html', async (c) => new Response(Bun.file('./public/admin/analytics.html')))
+app.get('/admin/traffic.html', async (c) => new Response(Bun.file('./public/admin/traffic.html')))
-// SPA routes
-app.get('/property/*', serveStatic({ path: './public/property.html' }))
-app.get('/catalog', serveStatic({ path: './public/catalog.html' }))
-app.get('/catalog.html', serveStatic({ path: './public/catalog.html' }))
-app.get('/admin', serveStatic({ path: './public/admin.html' }))
-app.get('/login', serveStatic({ path: './public/login.html' }))
+// SPA routes - using Bun.file() for static files to avoid content-length bugs
+app.get('/property/*', async (c) => new Response(Bun.file('./public/property.html')))
+app.get('/catalog', async (c) => new Response(Bun.file('./public/catalog.html')))
+app.get('/catalog.html', async (c) => new Response(Bun.file('./public/catalog.html')))
+app.get('/admin', async (c) => new Response(Bun.file('./public/admin.html')))
+app.get('/login', async (c) => new Response(Bun.file('./public/login.html')))
// Fallback to index.html for all other routes
-app.get('*', serveStatic({ path: './public/index.html' }))
+app.get('*', async (c) => new Response(Bun.file('./public/index.html')))
// Start server
const port = parseInt(process.env.PORT || '8080')