Merge pull request #203 from BEDOLAGA-DEV/dev

Release: dev → main
This commit is contained in:
Egor
2026-02-08 17:55:56 +03:00
committed by GitHub
6 changed files with 81 additions and 48 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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',
}} }}
/> />
</> </>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,