file uploaded path correction
This commit is contained in:
+35
-18
@@ -1,4 +1,6 @@
|
||||
import { API_BASE } from '../lib/apiBase';
|
||||
import { uploadLocal } from '../lib/localFileStore';
|
||||
import { notifyContentUpdated } from '../context/ContentContext';
|
||||
|
||||
/** Frontend-only fallback when the Node API is not reachable on the host. */
|
||||
const STATIC_USER = 'admin';
|
||||
@@ -45,7 +47,9 @@ async function apiFetch(path, options = {}) {
|
||||
: res.status === 502
|
||||
? 'API server unreachable. Start Node on the host (port 3001).'
|
||||
: 'Request failed';
|
||||
throw new Error(data.error || fallback);
|
||||
const err = new Error(data.error || fallback);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -65,7 +69,17 @@ export const adminApi = {
|
||||
|
||||
me: async () => {
|
||||
if (isStaticSession()) {
|
||||
return { username: STATIC_USER, mode: 'static' };
|
||||
// If server is now running, clear the stale static token so real login is used
|
||||
try {
|
||||
await apiFetch('/api/auth/me');
|
||||
// Server responded — clear static token so user re-authenticates properly
|
||||
clearToken();
|
||||
throw new Error('Session expired, please log in again');
|
||||
} catch (err) {
|
||||
if (err.status) throw err; // server is up but rejected — force re-login
|
||||
// Server unreachable — static session is fine
|
||||
return { username: STATIC_USER, mode: 'static' };
|
||||
}
|
||||
}
|
||||
return apiFetch('/api/auth/me');
|
||||
},
|
||||
@@ -82,14 +96,28 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
updateSection: async (section, data) => {
|
||||
try {
|
||||
return await apiFetch(`/api/content/${section}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
} catch {
|
||||
// Skip API entirely for static sessions
|
||||
if (isStaticSession()) {
|
||||
const saved = localStorage.getItem('static_content');
|
||||
const { default: seed } = await import('../../server/seed/default-content.json');
|
||||
const current = saved ? JSON.parse(saved) : seed;
|
||||
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
|
||||
localStorage.setItem('static_content', JSON.stringify(updated));
|
||||
notifyContentUpdated();
|
||||
return { message: `${section} updated (local)`, updatedAt: updated._updatedAt };
|
||||
}
|
||||
try {
|
||||
const result = await apiFetch(`/api/content/${section}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
notifyContentUpdated();
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.status === 401) clearToken();
|
||||
const saved = localStorage.getItem('static_content');
|
||||
const { default: seed } = await import('../../server/seed/default-content.json');
|
||||
const current = saved ? JSON.parse(saved) : seed;
|
||||
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
|
||||
localStorage.setItem('static_content', JSON.stringify(updated));
|
||||
notifyContentUpdated();
|
||||
return { message: `${section} updated (local)`, updatedAt: updated._updatedAt };
|
||||
}
|
||||
},
|
||||
@@ -103,18 +131,7 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
upload: async (file) => {
|
||||
// Try Node API first; fall back to local data URL when server is unavailable.
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return await apiFetch('/api/upload', { method: 'POST', body: form });
|
||||
} catch {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve({ url: reader.result, filename: file.name });
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
// Store file locally in IndexedDB — no server needed
|
||||
return uploadLocal(file);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export function ImageUpload({ value, onChange, label = 'Image' }) {
|
||||
const { url } = await adminApi.upload(file);
|
||||
onChange(url);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
+9
-5
@@ -2,9 +2,13 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './admin.css';
|
||||
import App from './App.jsx';
|
||||
import { restoreAllBlobUrls } from '../lib/localFileStore';
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
// Restore IndexedDB-stored file blob URLs before rendering
|
||||
restoreAllBlobUrls().then(() => {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,81 +2,113 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import defaultContent from '../../server/seed/default-content.json';
|
||||
import { API_BASE } from '../lib/apiBase';
|
||||
import { prefixAssets } from '../lib/assetUrl';
|
||||
import { restoreAllBlobUrls } from '../lib/localFileStore';
|
||||
|
||||
const ContentContext = createContext(null);
|
||||
|
||||
// BroadcastChannel works across tabs on the same origin
|
||||
const BC = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('citpl_content') : null;
|
||||
|
||||
// Called by admin api.js after every save to push update to all tabs
|
||||
export function notifyContentUpdated() {
|
||||
BC?.postMessage({ type: 'updated' });
|
||||
window.dispatchEvent(new CustomEvent('citpl:content-updated'));
|
||||
}
|
||||
|
||||
function allowedPreviewOrigins() {
|
||||
const origins = new Set(['https://demo.cavinkare.in/', 'https://demo.cavinkare.in']);
|
||||
if (typeof window !== 'undefined') origins.add(window.location.origin);
|
||||
return origins;
|
||||
}
|
||||
|
||||
function loadFromLocalStorage() {
|
||||
const saved = localStorage.getItem('static_content');
|
||||
if (!saved) return null;
|
||||
try {
|
||||
const { _updatedAt, ...rest } = JSON.parse(saved);
|
||||
return { content: prefixAssets(rest), updatedAt: _updatedAt ?? null };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function ContentProvider({ children, previewMode = false }) {
|
||||
const [content, setContent] = useState(prefixAssets(defaultContent));
|
||||
const [loading, setLoading] = useState(!previewMode);
|
||||
const [error, setError] = useState(null);
|
||||
const [updatedAt, setUpdatedAt] = useState(null);
|
||||
|
||||
function applyContent(data) {
|
||||
const { _updatedAt, ...rest } = data;
|
||||
setContent(prefixAssets(rest));
|
||||
setUpdatedAt(_updatedAt ?? null);
|
||||
setError(null);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
restoreAllBlobUrls().then(() => {
|
||||
fetch(`${API_BASE}/api/content`)
|
||||
.then((res) => res.ok ? res.json() : Promise.reject())
|
||||
.then((data) => applyContent(data))
|
||||
.catch(() => {
|
||||
const local = loadFromLocalStorage();
|
||||
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (previewMode) return;
|
||||
|
||||
fetch(`${API_BASE}/api/content`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to load content');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const { _updatedAt, ...rest } = data;
|
||||
setContent(prefixAssets(rest));
|
||||
setUpdatedAt(_updatedAt);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('Using fallback content:', err.message);
|
||||
const saved = localStorage.getItem('static_content');
|
||||
if (saved) {
|
||||
try {
|
||||
const { _updatedAt, ...rest } = JSON.parse(saved);
|
||||
setContent(prefixAssets(rest));
|
||||
setUpdatedAt(_updatedAt ?? null);
|
||||
return;
|
||||
} catch { /* ignore parse error */ }
|
||||
}
|
||||
setError(err.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
// Initial load
|
||||
restoreAllBlobUrls().then(() => {
|
||||
fetch(`${API_BASE}/api/content`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to load content');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => applyContent(data))
|
||||
.catch((err) => {
|
||||
console.warn('Using fallback content:', err.message);
|
||||
const local = loadFromLocalStorage();
|
||||
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||
else setError(err.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
});
|
||||
|
||||
// Cross-tab: BroadcastChannel (admin tab → main site tab)
|
||||
if (BC) BC.onmessage = (e) => { if (e.data?.type === 'updated') refresh(); };
|
||||
// Same-tab: custom event
|
||||
window.addEventListener('citpl:content-updated', refresh);
|
||||
// Cross-tab fallback: storage event
|
||||
const onStorage = (e) => { if (e.key === 'static_content') refresh(); };
|
||||
window.addEventListener('storage', onStorage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('citpl:content-updated', refresh);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
if (BC) BC.onmessage = null;
|
||||
};
|
||||
}, [previewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previewMode) return;
|
||||
|
||||
setLoading(false);
|
||||
const origins = allowedPreviewOrigins();
|
||||
|
||||
const onMessage = (event) => {
|
||||
if (!origins.has(event.origin)) return;
|
||||
if (event.data?.type !== 'ADMIN_PREVIEW' || !event.data.fullContent) return;
|
||||
|
||||
setContent(prefixAssets(event.data.fullContent));
|
||||
setLoading(false);
|
||||
|
||||
const anchor = event.data.anchor;
|
||||
if (!anchor) return;
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.getElementById(anchor)?.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
}, 180);
|
||||
};
|
||||
|
||||
window.addEventListener('message', onMessage);
|
||||
for (const origin of origins) {
|
||||
try {
|
||||
window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try { window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('message', onMessage);
|
||||
}, [previewMode]);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { blobCache } from './localFileStore';
|
||||
|
||||
/**
|
||||
* Vite base, e.g. "/citpl_website/" in production and "/" in local dev.
|
||||
* Runtime fallback: if an old/dev build baked base "/" but the page is
|
||||
@@ -26,6 +28,9 @@ export function assetUrl(path) {
|
||||
|
||||
if (/^(data:|blob:)/i.test(path)) return path;
|
||||
|
||||
// Resolve locally uploaded files from IndexedDB blob cache
|
||||
if (blobCache.has(path)) return blobCache.get(path);
|
||||
|
||||
const assetBase = resolveAssetBase();
|
||||
const prefix = assetBase.replace(/\/$/, '');
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Local file store using IndexedDB.
|
||||
* Stores uploaded files as binary blobs keyed by a stable path like /assets/filename.
|
||||
* On load, restores blob URLs so images/videos display correctly.
|
||||
*/
|
||||
|
||||
const DB_NAME = 'citpl_uploads';
|
||||
const STORE = 'files';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onupgradeneeded = (e) => e.target.result.createObjectStore(STORE);
|
||||
req.onsuccess = (e) => resolve(e.target.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveFile(key, file) {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readwrite');
|
||||
tx.objectStore(STORE).put({ blob: file, name: file.name, type: file.type }, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFileUrl(key) {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve) => {
|
||||
const req = db.transaction(STORE).objectStore(STORE).get(key);
|
||||
req.onsuccess = () => {
|
||||
if (req.result) resolve(URL.createObjectURL(req.result.blob));
|
||||
else resolve(null);
|
||||
};
|
||||
req.onerror = () => resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllKeys() {
|
||||
const db = await openDb();
|
||||
return new Promise((resolve) => {
|
||||
const req = db.transaction(STORE).objectStore(STORE).getAllKeys();
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => resolve([]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file locally and return a stable key path like /assets/user_upload_filename.ext
|
||||
*/
|
||||
export async function uploadLocal(file) {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
const base = file.name.replace(/[^a-zA-Z0-9-_]/g, '_').replace(/\.[^.]+$/, '').slice(0, 40);
|
||||
const key = `/assets/user_upload_${base}_${Date.now()}.${ext}`;
|
||||
await saveFile(key, file);
|
||||
const url = URL.createObjectURL(file);
|
||||
// Cache the blob URL in memory for this session
|
||||
blobCache.set(key, url);
|
||||
return { url: key, blobUrl: url };
|
||||
}
|
||||
|
||||
// In-memory cache: key → blob URL (valid for current session)
|
||||
export const blobCache = new Map();
|
||||
|
||||
/**
|
||||
* Call once on app init to restore blob URLs for all stored files.
|
||||
* Returns a Map of key → blobUrl.
|
||||
*/
|
||||
export async function restoreAllBlobUrls() {
|
||||
const keys = await getAllKeys();
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
if (!blobCache.has(key)) {
|
||||
const url = await getFileUrl(key);
|
||||
if (url) blobCache.set(key, url);
|
||||
}
|
||||
})
|
||||
);
|
||||
return blobCache;
|
||||
}
|
||||
+10
-7
@@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
import { ContentProvider } from './context/ContentContext.jsx'
|
||||
import { restoreAllBlobUrls } from './lib/localFileStore'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<ContentProvider>
|
||||
<App />
|
||||
</ContentProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
restoreAllBlobUrls().then(() => {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<ContentProvider>
|
||||
<App />
|
||||
</ContentProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user