Smooth analytics page reloads
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34s
This commit is contained in:
@@ -2,6 +2,57 @@ import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSu
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
const ANALYTICS_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const IN_FLIGHT_CACHE_TTL_MS = 15 * 1000;
|
||||
|
||||
type CacheOptions = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
type ApiCacheEntry<T> = {
|
||||
expiresAt: number;
|
||||
data?: T;
|
||||
promise?: Promise<T>;
|
||||
};
|
||||
|
||||
const analyticsCache = new Map<string, ApiCacheEntry<unknown>>();
|
||||
|
||||
const getCachedAnalyticsValue = <T>(key: string): T | undefined => {
|
||||
const entry = analyticsCache.get(key) as ApiCacheEntry<T> | undefined;
|
||||
if (!entry || entry.data === undefined || entry.expiresAt <= Date.now()) return undefined;
|
||||
return entry.data;
|
||||
};
|
||||
|
||||
const getCachedAnalytics = async <T>(key: string, loader: () => Promise<T>, options: CacheOptions = {}): Promise<T> => {
|
||||
const now = Date.now();
|
||||
const cached = analyticsCache.get(key) as ApiCacheEntry<T> | undefined;
|
||||
|
||||
if (!options.force) {
|
||||
if (cached?.data !== undefined && cached.expiresAt > now) return cached.data;
|
||||
if (cached?.promise && cached.expiresAt > now) return cached.promise;
|
||||
}
|
||||
|
||||
const promise = loader();
|
||||
analyticsCache.set(key, { expiresAt: now + IN_FLIGHT_CACHE_TTL_MS, promise });
|
||||
|
||||
try {
|
||||
const data = await promise;
|
||||
if (data === null) {
|
||||
analyticsCache.delete(key);
|
||||
} else {
|
||||
analyticsCache.set(key, { data, expiresAt: Date.now() + ANALYTICS_CACHE_TTL_MS });
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
analyticsCache.delete(key);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
|
||||
export const login = async (email: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
@@ -109,49 +160,46 @@ const authFetch = async (path: string, options: RequestInit = {}): Promise<Respo
|
||||
return response;
|
||||
};
|
||||
|
||||
export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<DashboardAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/dashboard?${params.toString()}`);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch dashboard analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise<DashboardAnalytics | null> => {
|
||||
const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch dashboard analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const fetchProductAnalytics = async (dateRange: DateRange): Promise<ProductAnalyticsItem[]> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/products?${params.toString()}`);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
const path = `/analytics/products?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchProductDetailsAnalytics = async (productId: string, dateRange: DateRange): Promise<ProductDetailsAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/details?${params.toString()}`);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
const path = `/analytics/products/${encodeURIComponent(productId)}/details?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Partial<ClientMetadataFilters>) => {
|
||||
@@ -162,65 +210,87 @@ const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Par
|
||||
};
|
||||
|
||||
export const fetchClientFilterOptions = async (dateRange: DateRange): Promise<ClientFilterOptions> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/clients/filters?${params.toString()}`);
|
||||
if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client filter options failed', error);
|
||||
return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
}
|
||||
const path = `/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client filter options failed', error);
|
||||
return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<RfmAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
const response = await authFetch(`/analytics/rfm?${params.toString()}`);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch RFM analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
const buildRfmAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
|
||||
const params = buildDateRangeParams(dateRange);
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
return `/analytics/rfm?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<ClientAnalyticsItem[]> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
const response = await authFetch(`/analytics/clients?${params.toString()}`);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
export const getCachedRfmAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): RfmAnalytics | undefined => {
|
||||
return getCachedAnalyticsValue<RfmAnalytics>(buildRfmAnalyticsPath(dateRange, filters));
|
||||
};
|
||||
|
||||
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<RfmAnalytics | null> => {
|
||||
const path = buildRfmAnalyticsPath(dateRange, filters);
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch RFM analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
}, options);
|
||||
};
|
||||
|
||||
const buildClientAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
|
||||
const params = buildDateRangeParams(dateRange);
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
return `/analytics/clients?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const getCachedClientAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): ClientAnalyticsItem[] | undefined => {
|
||||
return getCachedAnalyticsValue<ClientAnalyticsItem[]>(buildClientAnalyticsPath(dateRange, filters));
|
||||
};
|
||||
|
||||
export const getCachedClientFilterOptions = (dateRange: DateRange): ClientFilterOptions | undefined => {
|
||||
return getCachedAnalyticsValue<ClientFilterOptions>(`/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`);
|
||||
};
|
||||
|
||||
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<ClientAnalyticsItem[]> => {
|
||||
const path = buildClientAnalyticsPath(dateRange, filters);
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const fetchClientDetailsAnalytics = async (clientToken: string, dateRange: DateRange): Promise<ClientDetailsAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/clients/${encodeURIComponent(clientToken)}/details?${params.toString()}`);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
const path = `/analytics/clients/${encodeURIComponent(clientToken)}/details?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const clearAnalyticsCache = () => {
|
||||
analyticsCache.clear();
|
||||
};
|
||||
|
||||
export const fetchCampaigns = async (): Promise<CampaignQueueSummary | null> => {
|
||||
|
||||
Reference in New Issue
Block a user