mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -28,16 +28,19 @@ export interface AnalyticsCounters {
|
|||||||
const BRANDING_CACHE_KEY = 'cabinet_branding';
|
const BRANDING_CACHE_KEY = 'cabinet_branding';
|
||||||
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded';
|
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded';
|
||||||
|
|
||||||
|
// In-memory blob URL cache to avoid exposing backend URL
|
||||||
|
let _logoBlobUrl: string | null = null;
|
||||||
|
|
||||||
// Check if logo was already preloaded in this session
|
// Check if logo was already preloaded in this session
|
||||||
export const isLogoPreloaded = (): boolean => {
|
export const isLogoPreloaded = (): boolean => {
|
||||||
try {
|
try {
|
||||||
|
if (_logoBlobUrl) return true;
|
||||||
const cached = getCachedBranding();
|
const cached = getCachedBranding();
|
||||||
if (!cached?.has_custom_logo || !cached?.logo_url) {
|
if (!cached?.has_custom_logo || !cached?.logo_url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`;
|
|
||||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
||||||
return preloaded === logoUrl;
|
return preloaded === cached.logo_url;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -65,33 +68,42 @@ export const setCachedBranding = (branding: BrandingInfo) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preload logo image for instant display
|
// Preload logo image as blob to hide backend URL
|
||||||
export const preloadLogo = (branding: BrandingInfo): Promise<void> => {
|
export const preloadLogo = async (branding: BrandingInfo): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
if (!branding.has_custom_logo || !branding.logo_url) {
|
||||||
if (!branding.has_custom_logo || !branding.logo_url) {
|
return;
|
||||||
resolve();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check if already preloaded in this session
|
||||||
|
if (_logoBlobUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
||||||
|
if (preloaded === branding.logo_url && _logoBlobUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
|
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
|
||||||
|
const response = await fetch(logoUrl);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
// Check if already preloaded in this session
|
const blob = await response.blob();
|
||||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
// Revoke previous blob URL if exists
|
||||||
if (preloaded === logoUrl) {
|
if (_logoBlobUrl) {
|
||||||
resolve();
|
URL.revokeObjectURL(_logoBlobUrl);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
_logoBlobUrl = URL.createObjectURL(blob);
|
||||||
const img = new Image();
|
sessionStorage.setItem(LOGO_PRELOADED_KEY, branding.logo_url);
|
||||||
img.onload = () => {
|
} catch {
|
||||||
sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl);
|
// Fetch failed, logo will use letter fallback
|
||||||
resolve();
|
}
|
||||||
};
|
|
||||||
img.onerror = () => resolve();
|
|
||||||
img.src = logoUrl;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the blob URL for the logo (safe, doesn't expose backend)
|
||||||
|
export const getLogoBlobUrl = (): string | null => _logoBlobUrl;
|
||||||
|
|
||||||
// Initialize logo preload from cache on page load
|
// Initialize logo preload from cache on page load
|
||||||
export const initLogoPreload = () => {
|
export const initLogoPreload = () => {
|
||||||
const cached = getCachedBranding();
|
const cached = getCachedBranding();
|
||||||
@@ -122,21 +134,29 @@ export const brandingApi = {
|
|||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Invalidate cached blob so it gets re-fetched
|
||||||
|
if (_logoBlobUrl) {
|
||||||
|
URL.revokeObjectURL(_logoBlobUrl);
|
||||||
|
_logoBlobUrl = null;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(LOGO_PRELOADED_KEY);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Delete custom logo (admin only)
|
// Delete custom logo (admin only)
|
||||||
deleteLogo: async (): Promise<BrandingInfo> => {
|
deleteLogo: async (): Promise<BrandingInfo> => {
|
||||||
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo');
|
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo');
|
||||||
|
if (_logoBlobUrl) {
|
||||||
|
URL.revokeObjectURL(_logoBlobUrl);
|
||||||
|
_logoBlobUrl = null;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(LOGO_PRELOADED_KEY);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get logo URL (without cache busting - server handles caching via Cache-Control headers)
|
// Get logo URL as blob (hides backend URL from DOM)
|
||||||
getLogoUrl: (branding: BrandingInfo): string | null => {
|
getLogoUrl: (_branding: BrandingInfo): string | null => {
|
||||||
if (!branding.has_custom_logo || !branding.logo_url) {
|
return _logoBlobUrl;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get animation enabled (public, no auth required)
|
// Get animation enabled (public, no auth required)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function AppHeader({
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await brandingApi.getBranding();
|
const data = await brandingApi.getBranding();
|
||||||
setCachedBranding(data);
|
setCachedBranding(data);
|
||||||
preloadLogo(data);
|
await preloadLogo(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
initialData: getCachedBranding() ?? undefined,
|
initialData: getCachedBranding() ?? undefined,
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export function Aurora() {
|
|||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const renderer = new Renderer({
|
const renderer = new Renderer({
|
||||||
alpha: true,
|
alpha: true,
|
||||||
antialias: true,
|
antialias: false,
|
||||||
powerPreference: 'low-power',
|
powerPreference: 'low-power',
|
||||||
});
|
});
|
||||||
rendererRef.current = renderer;
|
rendererRef.current = renderer;
|
||||||
@@ -210,21 +210,34 @@ export function Aurora() {
|
|||||||
|
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
|
|
||||||
|
// Рендерим на 10% разрешения — шейдер обрабатывает в ~100 раз меньше пикселей.
|
||||||
|
// renderer.setSize задаёт и буфер, и CSS-размер канваса, поэтому после него
|
||||||
|
// принудительно возвращаем CSS на 100% — канвас растягивается браузером,
|
||||||
|
// а filter: blur() сглаживает артефакты масштабирования.
|
||||||
|
const RESOLUTION_SCALE = 0.1;
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
if (!containerRef.current || !rendererRef.current || !programRef.current) return;
|
if (!containerRef.current || !rendererRef.current || !programRef.current) return;
|
||||||
const w = containerRef.current.offsetWidth;
|
const fullW = containerRef.current.offsetWidth;
|
||||||
const h = containerRef.current.offsetHeight;
|
const fullH = containerRef.current.offsetHeight;
|
||||||
rendererRef.current.setSize(w, h);
|
const bufW = Math.max(Math.ceil(fullW * RESOLUTION_SCALE), 1);
|
||||||
programRef.current.uniforms.uResolution.value = [w, h];
|
const bufH = Math.max(Math.ceil(fullH * RESOLUTION_SCALE), 1);
|
||||||
|
rendererRef.current.setSize(bufW, bufH);
|
||||||
|
// OGL setSize ставит canvas.style.width/height = bufW/bufH px,
|
||||||
|
// перезаписываем на 100% чтобы CSS растянул маленький буфер на весь экран
|
||||||
|
const canvas = rendererRef.current.gl.canvas as HTMLCanvasElement;
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = '100%';
|
||||||
|
programRef.current.uniforms.uResolution.value = [bufW, bufH];
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
const targetFPS = 30;
|
const targetFPS = 20;
|
||||||
const frameInterval = 1000 / targetFPS;
|
const frameInterval = 1000 / targetFPS;
|
||||||
const speed = 0.3; // Slow and smooth
|
const speed = 0.3;
|
||||||
|
|
||||||
function animate(currentTime: number) {
|
function animate(currentTime: number) {
|
||||||
animationFrameRef.current = requestAnimationFrame(animate);
|
animationFrameRef.current = requestAnimationFrame(animate);
|
||||||
@@ -275,19 +288,19 @@ export function Aurora() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* WebGL Aurora canvas */}
|
{/* WebGL Aurora canvas — рендерится на ~10% разрешения, CSS растягивает обратно.
|
||||||
|
filter: blur() сглаживает артефакты масштабирования (дешевле backdrop-filter в ~10-20 раз,
|
||||||
|
т.к. блюрит только один элемент, а не все слои под ним). */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="pointer-events-none fixed inset-0 z-0"
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
/>
|
|
||||||
{/* Blur overlay */}
|
|
||||||
<div
|
|
||||||
className="pointer-events-none fixed inset-0 z-0"
|
className="pointer-events-none fixed inset-0 z-0"
|
||||||
style={{
|
style={{
|
||||||
backdropFilter: 'blur(80px)',
|
width: '100%',
|
||||||
WebkitBackdropFilter: 'blur(80px)',
|
height: '100%',
|
||||||
|
filter: 'blur(20px)',
|
||||||
|
WebkitFilter: 'blur(20px)',
|
||||||
backgroundColor: blurColor,
|
backgroundColor: blurColor,
|
||||||
|
contain: 'strict',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function DesktopSidebar({
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await brandingApi.getBranding();
|
const data = await brandingApi.getBranding();
|
||||||
setCachedBranding(data);
|
setCachedBranding(data);
|
||||||
preloadLogo(data);
|
await preloadLogo(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
initialData: getCachedBranding() ?? undefined,
|
initialData: getCachedBranding() ?? undefined,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function useBranding() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await brandingApi.getBranding();
|
const data = await brandingApi.getBranding();
|
||||||
setCachedBranding(data);
|
setCachedBranding(data);
|
||||||
preloadLogo(data);
|
await preloadLogo(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
initialData: getCachedBranding() ?? undefined,
|
initialData: getCachedBranding() ?? undefined,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function Login() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await brandingApi.getBranding();
|
const data = await brandingApi.getBranding();
|
||||||
setCachedBranding(data);
|
setCachedBranding(data);
|
||||||
preloadLogo(data);
|
await preloadLogo(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
staleTime: 60000,
|
staleTime: 60000,
|
||||||
|
|||||||
Reference in New Issue
Block a user