mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
- Branding cache moved from localStorage to sessionStorage so it clears when the mini-app is closed, preventing stale names after admin updates - Added initialDataUpdatedAt: 0 to all branding queries so fresh data is always fetched on page refresh while showing cached data instantly - One-time migration moves existing localStorage value to sessionStorage - Aurora component now checks WebGL availability upfront, skipping all hooks and subscriptions when WebGL is not supported
237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
import apiClient from './client';
|
|
|
|
export interface BrandingInfo {
|
|
name: string;
|
|
logo_url: string | null;
|
|
logo_letter: string;
|
|
has_custom_logo: boolean;
|
|
}
|
|
|
|
export interface AnimationEnabled {
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface FullscreenEnabled {
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface EmailAuthEnabled {
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface AnalyticsCounters {
|
|
yandex_metrika_id: string;
|
|
google_ads_id: string;
|
|
google_ads_label: string;
|
|
}
|
|
|
|
const BRANDING_CACHE_KEY = 'cabinet_branding';
|
|
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
|
|
export const isLogoPreloaded = (): boolean => {
|
|
try {
|
|
if (_logoBlobUrl) return true;
|
|
const cached = getCachedBranding();
|
|
if (!cached?.has_custom_logo || !cached?.logo_url) {
|
|
return false;
|
|
}
|
|
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
|
return preloaded === cached.logo_url;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Get cached branding from sessionStorage
|
|
export const getCachedBranding = (): BrandingInfo | null => {
|
|
try {
|
|
const cached = sessionStorage.getItem(BRANDING_CACHE_KEY);
|
|
if (cached) {
|
|
return JSON.parse(cached);
|
|
}
|
|
// One-time migration: move stale localStorage value to sessionStorage
|
|
const legacy = localStorage.getItem(BRANDING_CACHE_KEY);
|
|
if (legacy) {
|
|
localStorage.removeItem(BRANDING_CACHE_KEY);
|
|
sessionStorage.setItem(BRANDING_CACHE_KEY, legacy);
|
|
return JSON.parse(legacy);
|
|
}
|
|
} catch {
|
|
// storage not available or invalid JSON
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Update branding cache in sessionStorage
|
|
export const setCachedBranding = (branding: BrandingInfo) => {
|
|
try {
|
|
sessionStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding));
|
|
} catch {
|
|
// sessionStorage not available
|
|
}
|
|
};
|
|
|
|
// Preload logo image as blob to hide backend URL
|
|
export const preloadLogo = async (branding: BrandingInfo): Promise<void> => {
|
|
if (!branding.has_custom_logo || !branding.logo_url) {
|
|
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 response = await fetch(logoUrl);
|
|
if (!response.ok) return;
|
|
|
|
const blob = await response.blob();
|
|
// Revoke previous blob URL if exists
|
|
if (_logoBlobUrl) {
|
|
URL.revokeObjectURL(_logoBlobUrl);
|
|
}
|
|
_logoBlobUrl = URL.createObjectURL(blob);
|
|
sessionStorage.setItem(LOGO_PRELOADED_KEY, branding.logo_url);
|
|
} catch {
|
|
// Fetch failed, logo will use letter fallback
|
|
}
|
|
};
|
|
|
|
// 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
|
|
export const initLogoPreload = () => {
|
|
const cached = getCachedBranding();
|
|
if (cached) {
|
|
preloadLogo(cached);
|
|
}
|
|
};
|
|
|
|
export const brandingApi = {
|
|
// Get current branding (public, no auth required)
|
|
getBranding: async (): Promise<BrandingInfo> => {
|
|
const response = await apiClient.get<BrandingInfo>('/cabinet/branding');
|
|
return response.data;
|
|
},
|
|
|
|
// Update project name (admin only)
|
|
updateName: async (name: string): Promise<BrandingInfo> => {
|
|
const response = await apiClient.put<BrandingInfo>('/cabinet/branding/name', { name });
|
|
return response.data;
|
|
},
|
|
|
|
// Upload custom logo (admin only)
|
|
uploadLogo: async (file: File): Promise<BrandingInfo> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await apiClient.post<BrandingInfo>('/cabinet/branding/logo', formData, {
|
|
headers: {
|
|
'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;
|
|
},
|
|
|
|
// Delete custom logo (admin only)
|
|
deleteLogo: async (): Promise<BrandingInfo> => {
|
|
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo');
|
|
if (_logoBlobUrl) {
|
|
URL.revokeObjectURL(_logoBlobUrl);
|
|
_logoBlobUrl = null;
|
|
}
|
|
sessionStorage.removeItem(LOGO_PRELOADED_KEY);
|
|
return response.data;
|
|
},
|
|
|
|
// Get logo URL as blob (hides backend URL from DOM)
|
|
getLogoUrl: (_branding: BrandingInfo): string | null => {
|
|
return _logoBlobUrl;
|
|
},
|
|
|
|
// Get animation enabled (public, no auth required)
|
|
getAnimationEnabled: async (): Promise<AnimationEnabled> => {
|
|
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation');
|
|
return response.data;
|
|
},
|
|
|
|
// Update animation enabled (admin only)
|
|
updateAnimationEnabled: async (enabled: boolean): Promise<AnimationEnabled> => {
|
|
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', {
|
|
enabled,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Get fullscreen enabled (public, no auth required)
|
|
getFullscreenEnabled: async (): Promise<FullscreenEnabled> => {
|
|
try {
|
|
const response = await apiClient.get<FullscreenEnabled>('/cabinet/branding/fullscreen');
|
|
return response.data;
|
|
} catch {
|
|
// If endpoint doesn't exist, default to disabled
|
|
return { enabled: false };
|
|
}
|
|
},
|
|
|
|
// Update fullscreen enabled (admin only)
|
|
updateFullscreenEnabled: async (enabled: boolean): Promise<FullscreenEnabled> => {
|
|
const response = await apiClient.patch<FullscreenEnabled>('/cabinet/branding/fullscreen', {
|
|
enabled,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Get email auth enabled (public, no auth required)
|
|
getEmailAuthEnabled: async (): Promise<EmailAuthEnabled> => {
|
|
try {
|
|
const response = await apiClient.get<EmailAuthEnabled>('/cabinet/branding/email-auth');
|
|
return response.data;
|
|
} catch {
|
|
// If endpoint doesn't exist, default to enabled
|
|
return { enabled: true };
|
|
}
|
|
},
|
|
|
|
// Update email auth enabled (admin only)
|
|
updateEmailAuthEnabled: async (enabled: boolean): Promise<EmailAuthEnabled> => {
|
|
const response = await apiClient.patch<EmailAuthEnabled>('/cabinet/branding/email-auth', {
|
|
enabled,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Get analytics counters (public, no auth required)
|
|
getAnalyticsCounters: async (): Promise<AnalyticsCounters> => {
|
|
try {
|
|
const response = await apiClient.get<AnalyticsCounters>('/cabinet/branding/analytics');
|
|
return response.data;
|
|
} catch {
|
|
return { yandex_metrika_id: '', google_ads_id: '', google_ads_label: '' };
|
|
}
|
|
},
|
|
|
|
// Update analytics counters (admin only)
|
|
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
|
|
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);
|
|
return response.data;
|
|
},
|
|
};
|