file uploaded path correction
This commit is contained in:
+8
-4
@@ -14,7 +14,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
|
||||||
// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
|
// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
|
||||||
const SITE_BASE = (process.env.SITE_BASE || '/citpl_website').replace(/\/$/, '');
|
const SITE_BASE = (process.env.SITE_BASE ?? '').replace(/\/$/, '');
|
||||||
const distDir = path.join(__dirname, '..', 'dist');
|
const distDir = path.join(__dirname, '..', 'dist');
|
||||||
const publicAssetsDir = path.join(__dirname, '..', 'public', 'assets');
|
const publicAssetsDir = path.join(__dirname, '..', 'public', 'assets');
|
||||||
const serveStatic =
|
const serveStatic =
|
||||||
@@ -31,7 +31,11 @@ app.use(cors({
|
|||||||
credentials: true,
|
credentials: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(express.json({ limit: '10mb' }));
|
// Only parse JSON for non-multipart requests — multer handles multipart/form-data
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.headers['content-type']?.startsWith('multipart/form-data')) return next();
|
||||||
|
express.json({ limit: '10mb' })(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
function mountApp(base) {
|
function mountApp(base) {
|
||||||
const prefix = base || '';
|
const prefix = base || '';
|
||||||
@@ -48,8 +52,8 @@ function mountApp(base) {
|
|||||||
|
|
||||||
if (serveStatic) {
|
if (serveStatic) {
|
||||||
app.use(prefix || '/', express.static(distDir));
|
app.use(prefix || '/', express.static(distDir));
|
||||||
// SPA fallback only for the app's base path, not root-level unknown files
|
// SPA fallback — Express 5 requires named wildcard /*path (not /*)
|
||||||
app.get(`${prefix}/*`, (req, res, next) => {
|
app.get(`${prefix}/*path`, (req, res, next) => {
|
||||||
const ext = req.path.split('.').pop();
|
const ext = req.path.split('.').pop();
|
||||||
if (ext && ext !== req.path) return next();
|
if (ext && ext !== req.path) return next();
|
||||||
res.sendFile(path.join(distDir, 'index.html'));
|
res.sendFile(path.join(distDir, 'index.html'));
|
||||||
|
|||||||
+17
-1
@@ -33,7 +33,23 @@ const upload = multer({
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post('/', requireAuth, upload.single('file'), (req, res) => {
|
// Wrap multer for Express 5 — multer v2 doesn't handle Express 5 async errors natively
|
||||||
|
function runUpload(req, res) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
upload.single('file')(req, res, (err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await runUpload(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
|
||||||
|
return res.status(status).json({ error: err.message, code: err.code || 'UPLOAD_ERROR' });
|
||||||
|
}
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: 'No file uploaded', code: 'VALIDATION' });
|
return res.status(400).json({ error: 'No file uploaded', code: 'VALIDATION' });
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-17
@@ -1,4 +1,6 @@
|
|||||||
import { API_BASE } from '../lib/apiBase';
|
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. */
|
/** Frontend-only fallback when the Node API is not reachable on the host. */
|
||||||
const STATIC_USER = 'admin';
|
const STATIC_USER = 'admin';
|
||||||
@@ -45,7 +47,9 @@ async function apiFetch(path, options = {}) {
|
|||||||
: res.status === 502
|
: res.status === 502
|
||||||
? 'API server unreachable. Start Node on the host (port 3001).'
|
? 'API server unreachable. Start Node on the host (port 3001).'
|
||||||
: 'Request failed';
|
: 'Request failed';
|
||||||
throw new Error(data.error || fallback);
|
const err = new Error(data.error || fallback);
|
||||||
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -65,8 +69,18 @@ export const adminApi = {
|
|||||||
|
|
||||||
me: async () => {
|
me: async () => {
|
||||||
if (isStaticSession()) {
|
if (isStaticSession()) {
|
||||||
|
// 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 { username: STATIC_USER, mode: 'static' };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return apiFetch('/api/auth/me');
|
return apiFetch('/api/auth/me');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,14 +96,28 @@ export const adminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSection: async (section, data) => {
|
updateSection: async (section, data) => {
|
||||||
try {
|
// Skip API entirely for static sessions
|
||||||
return await apiFetch(`/api/content/${section}`, { method: 'PUT', body: JSON.stringify(data) });
|
if (isStaticSession()) {
|
||||||
} catch {
|
|
||||||
const saved = localStorage.getItem('static_content');
|
const saved = localStorage.getItem('static_content');
|
||||||
const { default: seed } = await import('../../server/seed/default-content.json');
|
const { default: seed } = await import('../../server/seed/default-content.json');
|
||||||
const current = saved ? JSON.parse(saved) : seed;
|
const current = saved ? JSON.parse(saved) : seed;
|
||||||
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
|
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
|
||||||
localStorage.setItem('static_content', JSON.stringify(updated));
|
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 };
|
return { message: `${section} updated (local)`, updatedAt: updated._updatedAt };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -103,18 +131,7 @@ export const adminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
upload: async (file) => {
|
upload: async (file) => {
|
||||||
// Try Node API first; fall back to local data URL when server is unavailable.
|
// Store file locally in IndexedDB — no server needed
|
||||||
try {
|
return uploadLocal(file);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function ImageUpload({ value, onChange, label = 'Image' }) {
|
|||||||
const { url } = await adminApi.upload(file);
|
const { url } = await adminApi.upload(file);
|
||||||
onChange(url);
|
onChange(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert('Upload failed: ' + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import { StrictMode } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import './admin.css';
|
import './admin.css';
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
|
import { restoreAllBlobUrls } from '../lib/localFileStore';
|
||||||
|
|
||||||
|
// Restore IndexedDB-stored file blob URLs before rendering
|
||||||
|
restoreAllBlobUrls().then(() => {
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,81 +2,113 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
import defaultContent from '../../server/seed/default-content.json';
|
import defaultContent from '../../server/seed/default-content.json';
|
||||||
import { API_BASE } from '../lib/apiBase';
|
import { API_BASE } from '../lib/apiBase';
|
||||||
import { prefixAssets } from '../lib/assetUrl';
|
import { prefixAssets } from '../lib/assetUrl';
|
||||||
|
import { restoreAllBlobUrls } from '../lib/localFileStore';
|
||||||
|
|
||||||
const ContentContext = createContext(null);
|
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() {
|
function allowedPreviewOrigins() {
|
||||||
const origins = new Set(['https://demo.cavinkare.in/', 'https://demo.cavinkare.in']);
|
const origins = new Set(['https://demo.cavinkare.in/', 'https://demo.cavinkare.in']);
|
||||||
if (typeof window !== 'undefined') origins.add(window.location.origin);
|
if (typeof window !== 'undefined') origins.add(window.location.origin);
|
||||||
return origins;
|
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 }) {
|
export function ContentProvider({ children, previewMode = false }) {
|
||||||
const [content, setContent] = useState(prefixAssets(defaultContent));
|
const [content, setContent] = useState(prefixAssets(defaultContent));
|
||||||
const [loading, setLoading] = useState(!previewMode);
|
const [loading, setLoading] = useState(!previewMode);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [updatedAt, setUpdatedAt] = 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(() => {
|
useEffect(() => {
|
||||||
if (previewMode) return;
|
if (previewMode) return;
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
restoreAllBlobUrls().then(() => {
|
||||||
fetch(`${API_BASE}/api/content`)
|
fetch(`${API_BASE}/api/content`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error('Failed to load content');
|
if (!res.ok) throw new Error('Failed to load content');
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => applyContent(data))
|
||||||
const { _updatedAt, ...rest } = data;
|
|
||||||
setContent(prefixAssets(rest));
|
|
||||||
setUpdatedAt(_updatedAt);
|
|
||||||
setError(null);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.warn('Using fallback content:', err.message);
|
console.warn('Using fallback content:', err.message);
|
||||||
const saved = localStorage.getItem('static_content');
|
const local = loadFromLocalStorage();
|
||||||
if (saved) {
|
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||||
try {
|
else setError(err.message);
|
||||||
const { _updatedAt, ...rest } = JSON.parse(saved);
|
|
||||||
setContent(prefixAssets(rest));
|
|
||||||
setUpdatedAt(_updatedAt ?? null);
|
|
||||||
return;
|
|
||||||
} catch { /* ignore parse error */ }
|
|
||||||
}
|
|
||||||
setError(err.message);
|
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.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]);
|
}, [previewMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!previewMode) return;
|
if (!previewMode) return;
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
const origins = allowedPreviewOrigins();
|
const origins = allowedPreviewOrigins();
|
||||||
|
|
||||||
const onMessage = (event) => {
|
const onMessage = (event) => {
|
||||||
if (!origins.has(event.origin)) return;
|
if (!origins.has(event.origin)) return;
|
||||||
if (event.data?.type !== 'ADMIN_PREVIEW' || !event.data.fullContent) return;
|
if (event.data?.type !== 'ADMIN_PREVIEW' || !event.data.fullContent) return;
|
||||||
|
|
||||||
setContent(prefixAssets(event.data.fullContent));
|
setContent(prefixAssets(event.data.fullContent));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const anchor = event.data.anchor;
|
const anchor = event.data.anchor;
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
document.getElementById(anchor)?.scrollIntoView({ behavior: 'auto', block: 'start' });
|
document.getElementById(anchor)?.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||||
}, 180);
|
}, 180);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', onMessage);
|
window.addEventListener('message', onMessage);
|
||||||
for (const origin of origins) {
|
for (const origin of origins) {
|
||||||
try {
|
try { window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin); } catch { /* ignore */ }
|
||||||
window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin);
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return () => window.removeEventListener('message', onMessage);
|
return () => window.removeEventListener('message', onMessage);
|
||||||
}, [previewMode]);
|
}, [previewMode]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { blobCache } from './localFileStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vite base, e.g. "/citpl_website/" in production and "/" in local dev.
|
* 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
|
* 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;
|
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 assetBase = resolveAssetBase();
|
||||||
const prefix = assetBase.replace(/\/$/, '');
|
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;
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import { ContentProvider } from './context/ContentContext.jsx'
|
import { ContentProvider } from './context/ContentContext.jsx'
|
||||||
|
import { restoreAllBlobUrls } from './lib/localFileStore'
|
||||||
|
|
||||||
|
restoreAllBlobUrls().then(() => {
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ContentProvider>
|
<ContentProvider>
|
||||||
@@ -11,3 +13,4 @@ createRoot(document.getElementById('root')).render(
|
|||||||
</ContentProvider>
|
</ContentProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|||||||
+10
-2
@@ -12,8 +12,16 @@ export default defineConfig((configEnv) => {
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:3001',
|
'/api': {
|
||||||
'/uploads': 'http://localhost:3001',
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 120000,
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
'/uploads': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user