feat(results-page): enhance search with caching, pagination, and UI tweaks
- Add dynamic rendering and Node.js runtime config to location API for better performance - Improve ProductCardSkeleton with dark mode styling for consistent theming - Simplify Hero search placeholder to focus on product name input - Integrate caching, pagination, and cache clearing in results page for optimized search experience and user control
This commit is contained in:
1 parent
166abeb64e
commit
e4ed666074
11 files changed
+1301
-86
No files matched your search
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Gerenciador de cache para buscas de produtos
|
||||
* Gerencia armazenamento, recuperação e expiração de dados em localStorage
|
||||
*/
|
||||
|
||||
import { Product } from "./openFoodFactsApi";
|
||||
|
||||
// Tipo para filtros que aceita valores primitivos
|
||||
type FilterValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| string[]
|
||||
| number[]
|
||||
| null
|
||||
| undefined;
|
||||
export type FilterRecord = Record<string, FilterValue>;
|
||||
|
||||
export interface CachedSearch {
|
||||
query: string;
|
||||
country: string;
|
||||
timestamp: number;
|
||||
products: Product[];
|
||||
totalCount: number;
|
||||
filters?: FilterRecord;
|
||||
searchId: string;
|
||||
}
|
||||
|
||||
export interface CacheConfig {
|
||||
ttl: number; // Time to live em milissegundos
|
||||
maxSize: number; // Tamanho máximo em bytes
|
||||
maxEntries: number; // Número máximo de entradas
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: CacheConfig = {
|
||||
ttl: 60 * 60 * 1000, // 1 hora
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
maxEntries: 10, // 10 buscas diferentes
|
||||
};
|
||||
|
||||
class CacheManager {
|
||||
private config: CacheConfig;
|
||||
private readonly CACHE_PREFIX = "nutriscan_search_";
|
||||
private readonly CACHE_INDEX_KEY = "nutriscan_cache_index";
|
||||
|
||||
constructor(config: Partial<CacheConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera chave única para a busca baseada na query e filtros
|
||||
*/
|
||||
private generateCacheKey(
|
||||
query: string,
|
||||
country: string,
|
||||
filters?: FilterRecord
|
||||
): string {
|
||||
const filterString = filters ? JSON.stringify(filters) : "";
|
||||
const keyData = `${query}|${country}|${filterString}`;
|
||||
return this.CACHE_PREFIX + btoa(keyData).replace(/[^a-zA-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera ID único para a busca
|
||||
*/
|
||||
private generateSearchId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o cache está válido (não expirado)
|
||||
*/
|
||||
private isCacheValid(timestamp: number): boolean {
|
||||
return Date.now() - timestamp < this.config.ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém índice de caches armazenados
|
||||
*/
|
||||
private getCacheIndex(): string[] {
|
||||
try {
|
||||
const index = localStorage.getItem(this.CACHE_INDEX_KEY);
|
||||
return index ? JSON.parse(index) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza índice de caches
|
||||
*/
|
||||
private updateCacheIndex(keys: string[]): void {
|
||||
try {
|
||||
localStorage.setItem(this.CACHE_INDEX_KEY, JSON.stringify(keys));
|
||||
} catch (error) {
|
||||
console.warn("Erro ao atualizar índice de cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula tamanho aproximado de um objeto em bytes
|
||||
*/
|
||||
private getObjectSize(obj: unknown): number {
|
||||
return new Blob([JSON.stringify(obj)]).size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa caches antigos quando necessário
|
||||
*/
|
||||
private cleanupOldCaches(): void {
|
||||
try {
|
||||
const index = this.getCacheIndex();
|
||||
const validKeys: string[] = [];
|
||||
|
||||
// Remove caches expirados
|
||||
for (const key of index) {
|
||||
const cached = localStorage.getItem(key);
|
||||
if (cached) {
|
||||
try {
|
||||
const data: CachedSearch = JSON.parse(cached);
|
||||
if (this.isCacheValid(data.timestamp)) {
|
||||
validKeys.push(key);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch {
|
||||
// Remove cache corrompido
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Se ainda temos muitos caches, remove os mais antigos
|
||||
if (validKeys.length > this.config.maxEntries) {
|
||||
const sortedKeys = validKeys.sort((a, b) => {
|
||||
const aData = JSON.parse(localStorage.getItem(a) || "{}");
|
||||
const bData = JSON.parse(localStorage.getItem(b) || "{}");
|
||||
return aData.timestamp - bData.timestamp;
|
||||
});
|
||||
|
||||
const keysToRemove = sortedKeys.slice(
|
||||
0,
|
||||
validKeys.length - this.config.maxEntries
|
||||
);
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
validKeys.splice(0, keysToRemove.length);
|
||||
}
|
||||
|
||||
this.updateCacheIndex(validKeys);
|
||||
} catch (error) {
|
||||
console.warn("Erro ao limpar caches antigos:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva resultado da busca no cache
|
||||
*/
|
||||
saveSearch(
|
||||
query: string,
|
||||
country: string,
|
||||
products: Product[],
|
||||
totalCount: number,
|
||||
filters?: FilterRecord
|
||||
): string {
|
||||
try {
|
||||
this.cleanupOldCaches();
|
||||
|
||||
const searchId = this.generateSearchId();
|
||||
const cacheKey = this.generateCacheKey(query, country, filters);
|
||||
|
||||
const cachedData: CachedSearch = {
|
||||
query,
|
||||
country,
|
||||
timestamp: Date.now(),
|
||||
products,
|
||||
totalCount,
|
||||
filters,
|
||||
searchId,
|
||||
};
|
||||
|
||||
// Verifica se o tamanho não excede o limite
|
||||
const dataSize = this.getObjectSize(cachedData);
|
||||
if (dataSize > this.config.maxSize) {
|
||||
console.warn("Dados muito grandes para cache, salvando apenas metade");
|
||||
cachedData.products = products.slice(
|
||||
0,
|
||||
Math.floor(products.length / 2)
|
||||
);
|
||||
}
|
||||
|
||||
localStorage.setItem(cacheKey, JSON.stringify(cachedData));
|
||||
|
||||
// Atualiza índice
|
||||
const index = this.getCacheIndex();
|
||||
if (!index.includes(cacheKey)) {
|
||||
index.push(cacheKey);
|
||||
this.updateCacheIndex(index);
|
||||
}
|
||||
|
||||
return searchId;
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar cache:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera resultado da busca do cache
|
||||
*/
|
||||
getSearch(
|
||||
query: string,
|
||||
country: string,
|
||||
filters?: FilterRecord
|
||||
): CachedSearch | null {
|
||||
try {
|
||||
const cacheKey = this.generateCacheKey(query, country, filters);
|
||||
const cached = localStorage.getItem(cacheKey);
|
||||
|
||||
if (!cached) return null;
|
||||
|
||||
const data: CachedSearch = JSON.parse(cached);
|
||||
|
||||
if (!this.isCacheValid(data.timestamp)) {
|
||||
localStorage.removeItem(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Erro ao recuperar cache:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache específico
|
||||
*/
|
||||
removeSearch(
|
||||
query: string,
|
||||
country: string,
|
||||
filters?: FilterRecord
|
||||
): boolean {
|
||||
try {
|
||||
const cacheKey = this.generateCacheKey(query, country, filters);
|
||||
localStorage.removeItem(cacheKey);
|
||||
|
||||
const index = this.getCacheIndex();
|
||||
const newIndex = index.filter((key) => key !== cacheKey);
|
||||
this.updateCacheIndex(newIndex);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Erro ao remover cache:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todos os caches
|
||||
*/
|
||||
clearAllCaches(): void {
|
||||
try {
|
||||
const index = this.getCacheIndex();
|
||||
index.forEach((key) => localStorage.removeItem(key));
|
||||
localStorage.removeItem(this.CACHE_INDEX_KEY);
|
||||
} catch (error) {
|
||||
console.error("Erro ao limpar todos os caches:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas do cache
|
||||
*/
|
||||
getCacheStats(): {
|
||||
totalEntries: number;
|
||||
totalSize: number;
|
||||
oldestEntry: number | null;
|
||||
newestEntry: number | null;
|
||||
} {
|
||||
try {
|
||||
const index = this.getCacheIndex();
|
||||
let totalSize = 0;
|
||||
let oldestTimestamp = null;
|
||||
let newestTimestamp = null;
|
||||
|
||||
for (const key of index) {
|
||||
const cached = localStorage.getItem(key);
|
||||
if (cached) {
|
||||
totalSize += this.getObjectSize(cached);
|
||||
try {
|
||||
const data: CachedSearch = JSON.parse(cached);
|
||||
if (!oldestTimestamp || data.timestamp < oldestTimestamp) {
|
||||
oldestTimestamp = data.timestamp;
|
||||
}
|
||||
if (!newestTimestamp || data.timestamp > newestTimestamp) {
|
||||
newestTimestamp = data.timestamp;
|
||||
}
|
||||
} catch {
|
||||
// Ignora caches corrompidos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalEntries: index.length,
|
||||
totalSize,
|
||||
oldestEntry: oldestTimestamp,
|
||||
newestEntry: newestTimestamp,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter estatísticas do cache:", error);
|
||||
return {
|
||||
totalEntries: 0,
|
||||
totalSize: 0,
|
||||
oldestEntry: null,
|
||||
newestEntry: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se há espaço suficiente no localStorage
|
||||
*/
|
||||
hasSpaceForData(dataSize: number): boolean {
|
||||
try {
|
||||
const stats = this.getCacheStats();
|
||||
return stats.totalSize + dataSize < this.config.maxSize;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instância singleton
|
||||
export const cacheManager = new CacheManager();
|
||||
|
||||
// Hook para usar o cache manager
|
||||
export function useCacheManager() {
|
||||
return {
|
||||
saveSearch: cacheManager.saveSearch.bind(cacheManager),
|
||||
getSearch: cacheManager.getSearch.bind(cacheManager),
|
||||
removeSearch: cacheManager.removeSearch.bind(cacheManager),
|
||||
clearAllCaches: cacheManager.clearAllCaches.bind(cacheManager),
|
||||
getCacheStats: cacheManager.getCacheStats.bind(cacheManager),
|
||||
hasSpaceForData: cacheManager.hasSpaceForData.bind(cacheManager),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user