feat(ui): enhance footer with NutriScan branding and fix database path
- Redesigned Footer.tsx with comprehensive layout including brand section, navigation links, social media icons, and contact info to improve user engagement and brand visibility - Updated DATABASE_URL in .env to correct Prisma database path for proper configuration and connectivity - Updated dependencies to support new UI components and ensure compatibility with enhanced features
This commit is contained in:
1 parent
8d6cbeb0bb
commit
9a4e4c762e
51 files changed
+7389
-218
No files matched your search
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Utilitário para detecção de país do usuário
|
||||
* Usa detecção de IP server-side via API Route
|
||||
*/
|
||||
|
||||
interface CountryInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Cache para evitar múltiplas requisições
|
||||
const CACHE_KEY = "nutriscan_user_country";
|
||||
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 horas
|
||||
|
||||
/**
|
||||
* Verifica se o cache ainda é válido
|
||||
*/
|
||||
function isCacheValid(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (!cached) return false;
|
||||
|
||||
const { timestamp } = JSON.parse(cached);
|
||||
return Date.now() - timestamp < CACHE_DURATION;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém país do cache
|
||||
*/
|
||||
function getCachedCountry(): CountryInfo | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (!cached) return null;
|
||||
|
||||
const { data } = JSON.parse(cached);
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva país no cache
|
||||
*/
|
||||
function saveToCache(country: CountryInfo): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
CACHE_KEY,
|
||||
JSON.stringify({
|
||||
data: country,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Erro ao salvar cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta o país do usuário usando detecção de IP server-side
|
||||
* 1. Verifica cache local primeiro
|
||||
* 2. Chama API Route /api/location que usa ip-api.com
|
||||
* 3. Fallback para Brasil em caso de erro
|
||||
*/
|
||||
export async function detectUserCountry(
|
||||
forceRefresh: boolean = false
|
||||
): Promise<CountryInfo> {
|
||||
// 1. Verificar cache primeiro (a menos que forceRefresh seja true)
|
||||
if (!forceRefresh && isCacheValid()) {
|
||||
const cached = getCachedCountry();
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Chamar API Route que detecta país por IP
|
||||
const response = await fetch("/api/location");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const countryInfo: CountryInfo = {
|
||||
code: data.countryCode,
|
||||
name: data.country,
|
||||
};
|
||||
|
||||
saveToCache(countryInfo);
|
||||
return countryInfo;
|
||||
} catch (error) {
|
||||
console.warn("Country detection failed:", error);
|
||||
|
||||
// 3. Fallback: Brasil
|
||||
const fallback: CountryInfo = { code: "br", name: "Brazil" };
|
||||
saveToCache(fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa o cache de país
|
||||
*/
|
||||
export function clearCountryCache(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações do país atual (síncrono)
|
||||
* Retorna null se não houver cache
|
||||
*/
|
||||
export function getCurrentCountry(): CountryInfo | null {
|
||||
if (isCacheValid()) {
|
||||
return getCachedCountry();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o usuário está no Brasil
|
||||
*/
|
||||
export function isBrazil(): boolean {
|
||||
const country = getCurrentCountry();
|
||||
return country?.code === "br" || country?.code === "brazil";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém o código do país para a API Open Food Facts
|
||||
*/
|
||||
export function getCountryCodeForAPI(): string {
|
||||
const country = getCurrentCountry();
|
||||
return country?.code || "br";
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* Wrapper para a API Open Food Facts
|
||||
* Centraliza todas as chamadas para a API e trata erros
|
||||
*/
|
||||
|
||||
export interface Product {
|
||||
code: string;
|
||||
product_name: string;
|
||||
brands?: string;
|
||||
categories?: string;
|
||||
ingredients_text?: string;
|
||||
nutriments?: {
|
||||
energy_100g?: number;
|
||||
fat_100g?: number;
|
||||
carbohydrates_100g?: number;
|
||||
proteins_100g?: number;
|
||||
sugars_100g?: number;
|
||||
fiber_100g?: number;
|
||||
sodium_100g?: number;
|
||||
};
|
||||
nutrition_grades?: string;
|
||||
nova_group?: number;
|
||||
image_url?: string;
|
||||
image_small_url?: string;
|
||||
image_front_url?: string;
|
||||
image_front_small_url?: string;
|
||||
countries?: string;
|
||||
countries_tags?: string[];
|
||||
allergens_tags?: string[];
|
||||
additives_tags?: string[];
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
products: Product[];
|
||||
count: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
skip: number;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// Configurações da API - agora usando API Routes do Next.js
|
||||
const API_BASE_URL = ""; // Usar rotas relativas para API Routes
|
||||
|
||||
/**
|
||||
* Faz uma requisição para a API com tratamento de erros
|
||||
*/
|
||||
async function makeApiRequest<T>(endpoint: string): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Erro na API:", error);
|
||||
throw new Error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Erro desconhecido ao consultar a API"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um produto específico por código de barras
|
||||
*/
|
||||
export async function searchByBarcode(
|
||||
barcode: string
|
||||
): Promise<Product | null> {
|
||||
try {
|
||||
// Limpar código de barras (remover espaços e caracteres não numéricos)
|
||||
const cleanBarcode = barcode.replace(/\D/g, "");
|
||||
|
||||
if (cleanBarcode.length < 8 || cleanBarcode.length > 13) {
|
||||
throw new Error("Código de barras deve ter entre 8 e 13 dígitos");
|
||||
}
|
||||
|
||||
const data = await makeApiRequest<Product>(`/api/products/${cleanBarcode}`);
|
||||
|
||||
return data || null;
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar produto por código:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca produtos por nome
|
||||
*/
|
||||
export async function searchByName(
|
||||
query: string,
|
||||
country?: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 20
|
||||
): Promise<SearchResult> {
|
||||
try {
|
||||
if (!query.trim()) {
|
||||
throw new Error("Termo de busca não pode estar vazio");
|
||||
}
|
||||
|
||||
// Construir parâmetros da URL para API Route
|
||||
const params = new URLSearchParams({
|
||||
q: query.trim(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
});
|
||||
|
||||
// Adicionar filtro de país se especificado
|
||||
if (country) {
|
||||
params.append("country", country);
|
||||
}
|
||||
|
||||
const data = await makeApiRequest<SearchResult>(
|
||||
`/api/search?${params.toString()}`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar produtos por nome:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca produtos por categoria
|
||||
*/
|
||||
export async function searchByCategory(
|
||||
category: string,
|
||||
country?: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 20
|
||||
): Promise<SearchResult> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
categories_tags_en: category,
|
||||
page_size: pageSize.toString(),
|
||||
page: page.toString(),
|
||||
json: "true",
|
||||
fields: [
|
||||
"code",
|
||||
"product_name",
|
||||
"brands",
|
||||
"categories",
|
||||
"image_url",
|
||||
"image_small_url",
|
||||
"nutrition_grades",
|
||||
"nova_group",
|
||||
].join(","),
|
||||
});
|
||||
|
||||
if (country) {
|
||||
params.append("countries_tags_en", country);
|
||||
}
|
||||
|
||||
// Chamada direta à API externa (temporária)
|
||||
const response = await fetch(
|
||||
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar produtos por categoria:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém categorias populares
|
||||
*/
|
||||
export async function getPopularCategories(
|
||||
country?: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
json: "true",
|
||||
fields: "categories_tags",
|
||||
});
|
||||
|
||||
if (country) {
|
||||
params.append("countries_tags_en", country);
|
||||
}
|
||||
|
||||
// Chamada direta à API externa (temporária)
|
||||
const response = await fetch(
|
||||
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return (
|
||||
data.facets?.categories_tags
|
||||
?.slice(0, 20)
|
||||
.map((cat: { name: string; count: number }) => cat.name) || []
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter categorias:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se um código de barras tem formato válido
|
||||
*/
|
||||
export function isValidBarcode(barcode: string): boolean {
|
||||
const cleanBarcode = barcode.replace(/\D/g, "");
|
||||
return cleanBarcode.length >= 8 && cleanBarcode.length <= 13;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta o tipo de busca baseado na entrada do usuário
|
||||
*/
|
||||
export function detectSearchType(query: string): "barcode" | "name" {
|
||||
const cleanQuery = query.trim();
|
||||
|
||||
// Se contém apenas números e tem 8-13 dígitos, é código de barras
|
||||
if (/^\d{8,13}$/.test(cleanQuery)) {
|
||||
return "barcode";
|
||||
}
|
||||
|
||||
// Caso contrário, é busca por nome
|
||||
return "name";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata dados nutricionais para exibição
|
||||
*/
|
||||
export function formatNutritionData(product: Product) {
|
||||
const nutriments = product.nutriments;
|
||||
if (!nutriments) return null;
|
||||
|
||||
// Função auxiliar para converter e formatar valores
|
||||
const formatValue = (
|
||||
value: number | undefined,
|
||||
decimals: number = 1
|
||||
): string => {
|
||||
if (value === undefined || value === null) return "N/A";
|
||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return "N/A";
|
||||
return decimals === 0
|
||||
? `${Math.round(numValue)}`
|
||||
: `${numValue.toFixed(decimals)}`;
|
||||
};
|
||||
|
||||
return {
|
||||
energy: nutriments.energy_100g
|
||||
? `${formatValue(nutriments.energy_100g, 0)} kcal`
|
||||
: "N/A",
|
||||
fat: nutriments.fat_100g ? `${formatValue(nutriments.fat_100g)}g` : "N/A",
|
||||
carbohydrates: nutriments.carbohydrates_100g
|
||||
? `${formatValue(nutriments.carbohydrates_100g)}g`
|
||||
: "N/A",
|
||||
proteins: nutriments.proteins_100g
|
||||
? `${formatValue(nutriments.proteins_100g)}g`
|
||||
: "N/A",
|
||||
sugars: nutriments.sugars_100g
|
||||
? `${formatValue(nutriments.sugars_100g)}g`
|
||||
: "N/A",
|
||||
fiber: nutriments.fiber_100g
|
||||
? `${formatValue(nutriments.fiber_100g)}g`
|
||||
: "N/A",
|
||||
sodium: nutriments.sodium_100g
|
||||
? `${formatValue(nutriments.sodium_100g)}g`
|
||||
: "N/A",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém a URL da imagem do produto (prioriza front, depois small)
|
||||
*/
|
||||
export function getProductImageUrl(product: Product): string | null {
|
||||
return (
|
||||
product.image_front_url ||
|
||||
product.image_front_small_url ||
|
||||
product.image_url ||
|
||||
product.image_small_url ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula pontuação de completude de um produto
|
||||
*/
|
||||
function calculateCompletenessScore(product: Product): number {
|
||||
let score = 0;
|
||||
if (product.product_name) score += 10;
|
||||
if (product.brands) score += 10;
|
||||
if (product.image_front_url || product.image_url) score += 15;
|
||||
if (product.nutriments?.energy_100g) score += 20;
|
||||
if (product.nutrition_grades) score += 15;
|
||||
if (product.nova_group) score += 10;
|
||||
if (product.ingredients_text) score += 10;
|
||||
if (product.categories) score += 10;
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordena produtos com base em critérios especiais
|
||||
*/
|
||||
export function sortProducts(
|
||||
products: Product[],
|
||||
sortBy: string,
|
||||
sortOrder: "asc" | "desc" = "asc"
|
||||
): Product[] {
|
||||
const sorted = [...products].sort((a, b) => {
|
||||
let comparison = 0;
|
||||
|
||||
switch (sortBy) {
|
||||
case "with_images":
|
||||
const aHasImage = !!(a.image_front_url || a.image_url);
|
||||
const bHasImage = !!(b.image_front_url || b.image_url);
|
||||
comparison = (bHasImage ? 1 : 0) - (aHasImage ? 1 : 0);
|
||||
break;
|
||||
|
||||
case "with_nutrition":
|
||||
const aHasNutrition = !!a.nutriments?.energy_100g;
|
||||
const bHasNutrition = !!b.nutriments?.energy_100g;
|
||||
comparison = (bHasNutrition ? 1 : 0) - (aHasNutrition ? 1 : 0);
|
||||
break;
|
||||
|
||||
case "completeness":
|
||||
const aScore = calculateCompletenessScore(a);
|
||||
const bScore = calculateCompletenessScore(b);
|
||||
comparison = bScore - aScore;
|
||||
break;
|
||||
|
||||
case "nova_group":
|
||||
comparison = (a.nova_group || 5) - (b.nova_group || 5);
|
||||
break;
|
||||
|
||||
case "eco_score":
|
||||
// Para eco_score, usar nutri-score como proxy (A = melhor eco)
|
||||
const aGrade = a.nutrition_grades?.toLowerCase();
|
||||
const bGrade = b.nutrition_grades?.toLowerCase();
|
||||
const gradeOrder = { a: 1, b: 2, c: 3, d: 4, e: 5 };
|
||||
const aGradeValue = gradeOrder[aGrade as keyof typeof gradeOrder] || 6;
|
||||
const bGradeValue = gradeOrder[bGrade as keyof typeof gradeOrder] || 6;
|
||||
comparison = aGradeValue - bGradeValue;
|
||||
break;
|
||||
|
||||
case "relevance":
|
||||
// Para relevância, usar completude + nutri-score
|
||||
const aRelevance =
|
||||
calculateCompletenessScore(a) + (a.nutrition_grades ? 20 : 0);
|
||||
const bRelevance =
|
||||
calculateCompletenessScore(b) + (b.nutrition_grades ? 20 : 0);
|
||||
comparison = bRelevance - aRelevance;
|
||||
break;
|
||||
|
||||
case "nutrition_grade":
|
||||
const aNutriGrade = a.nutrition_grades?.toLowerCase();
|
||||
const bNutriGrade = b.nutrition_grades?.toLowerCase();
|
||||
const gradeOrderForNutri = { a: 1, b: 2, c: 3, d: 4, e: 5 };
|
||||
const aNutriValue =
|
||||
gradeOrderForNutri[aNutriGrade as keyof typeof gradeOrderForNutri] ||
|
||||
6;
|
||||
const bNutriValue =
|
||||
gradeOrderForNutri[bNutriGrade as keyof typeof gradeOrderForNutri] ||
|
||||
6;
|
||||
comparison = aNutriValue - bNutriValue;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
comparison = (a.product_name || "").localeCompare(b.product_name || "");
|
||||
break;
|
||||
|
||||
case "brand":
|
||||
comparison = (a.brands || "").localeCompare(b.brands || "");
|
||||
break;
|
||||
|
||||
case "energy":
|
||||
const aEnergy = a.nutriments?.energy_100g || 0;
|
||||
const bEnergy = b.nutriments?.energy_100g || 0;
|
||||
comparison = aEnergy - bEnergy;
|
||||
break;
|
||||
|
||||
case "fat":
|
||||
const aFat = a.nutriments?.fat_100g || 0;
|
||||
const bFat = b.nutriments?.fat_100g || 0;
|
||||
comparison = aFat - bFat;
|
||||
break;
|
||||
|
||||
case "sugars":
|
||||
const aSugars = a.nutriments?.sugars_100g || 0;
|
||||
const bSugars = b.nutriments?.sugars_100g || 0;
|
||||
comparison = aSugars - bSugars;
|
||||
break;
|
||||
|
||||
default:
|
||||
comparison = 0;
|
||||
}
|
||||
|
||||
return sortOrder === "asc" ? comparison : -comparison;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Tipos para sistema de filtros de produtos
|
||||
* Baseado na API do Open Food Facts
|
||||
*/
|
||||
|
||||
export interface ProductFilters {
|
||||
// Filtros de classificação nutricional
|
||||
nutritionGrades?: ("a" | "b" | "c" | "d" | "e")[];
|
||||
novaGroups?: (1 | 2 | 3 | 4)[];
|
||||
|
||||
// Filtros de categoria e marca
|
||||
categories?: string[];
|
||||
brands?: string[];
|
||||
|
||||
// Filtros de ingredientes e aditivos
|
||||
allergens?: string[];
|
||||
excludeAllergens?: string[];
|
||||
additives?: string[];
|
||||
excludeAdditives?: string[];
|
||||
noAdditives?: boolean;
|
||||
|
||||
// Filtros nutricionais (valores por 100g)
|
||||
nutritionRanges?: {
|
||||
energy?: { min?: number; max?: number };
|
||||
fat?: { min?: number; max?: number };
|
||||
carbohydrates?: { min?: number; max?: number };
|
||||
proteins?: { min?: number; max?: number };
|
||||
sugars?: { min?: number; max?: number };
|
||||
fiber?: { min?: number; max?: number };
|
||||
sodium?: { min?: number; max?: number };
|
||||
};
|
||||
|
||||
// Filtros de país
|
||||
countries?: string[];
|
||||
|
||||
// Ordenação
|
||||
sortBy?:
|
||||
| "relevance"
|
||||
| "nutrition_grade"
|
||||
| "name"
|
||||
| "brand"
|
||||
| "energy"
|
||||
| "fat"
|
||||
| "sugars";
|
||||
sortOrder?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
count?: number;
|
||||
color?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface NutritionGrade {
|
||||
grade: "a" | "b" | "c" | "d" | "e";
|
||||
label: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface NovaGroup {
|
||||
group: 1 | 2 | 3 | 4;
|
||||
label: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SortOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface FilterState {
|
||||
filters: ProductFilters;
|
||||
isLoading: boolean;
|
||||
hasActiveFilters: boolean;
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
export interface FilterSidebarProps {
|
||||
filters: ProductFilters;
|
||||
onFiltersChange: (filters: ProductFilters) => void;
|
||||
onClearFilters: () => void;
|
||||
isLoading?: boolean;
|
||||
totalResults?: number;
|
||||
}
|
||||
|
||||
// Constantes para configuração dos filtros
|
||||
export const NUTRITION_GRADES: NutritionGrade[] = [
|
||||
{
|
||||
grade: "a",
|
||||
label: "A",
|
||||
color: "bg-green-500",
|
||||
description: "Muito bom",
|
||||
},
|
||||
{
|
||||
grade: "b",
|
||||
label: "B",
|
||||
color: "bg-green-400",
|
||||
description: "Bom",
|
||||
},
|
||||
{
|
||||
grade: "c",
|
||||
label: "C",
|
||||
color: "bg-yellow-500",
|
||||
description: "Regular",
|
||||
},
|
||||
{
|
||||
grade: "d",
|
||||
label: "D",
|
||||
color: "bg-orange-500",
|
||||
description: "Ruim",
|
||||
},
|
||||
{
|
||||
grade: "e",
|
||||
label: "E",
|
||||
color: "bg-red-500",
|
||||
description: "Muito ruim",
|
||||
},
|
||||
];
|
||||
|
||||
export const NOVA_GROUPS: NovaGroup[] = [
|
||||
{
|
||||
group: 1,
|
||||
label: "1",
|
||||
color: "bg-green-500",
|
||||
description: "Alimentos não processados ou minimamente processados",
|
||||
},
|
||||
{
|
||||
group: 2,
|
||||
label: "2",
|
||||
color: "bg-yellow-500",
|
||||
description: "Ingredientes culinários processados",
|
||||
},
|
||||
{
|
||||
group: 3,
|
||||
label: "3",
|
||||
color: "bg-orange-500",
|
||||
description: "Alimentos processados",
|
||||
},
|
||||
{
|
||||
group: 4,
|
||||
label: "4",
|
||||
color: "bg-red-500",
|
||||
description: "Alimentos ultraprocessados",
|
||||
},
|
||||
];
|
||||
|
||||
export const SORT_OPTIONS: SortOption[] = [
|
||||
{
|
||||
value: "relevance",
|
||||
label: "Relevância",
|
||||
description: "Resultados mais relevantes primeiro",
|
||||
icon: "TrendingUp",
|
||||
},
|
||||
{
|
||||
value: "nutrition_grade",
|
||||
label: "Nutri-Score",
|
||||
description: "Melhor classificação nutricional primeiro",
|
||||
icon: "Zap",
|
||||
},
|
||||
{
|
||||
value: "name",
|
||||
label: "Nome (A-Z)",
|
||||
description: "Ordenar por nome do produto",
|
||||
icon: "AlphabeticalOrder",
|
||||
},
|
||||
{
|
||||
value: "brand",
|
||||
label: "Marca (A-Z)",
|
||||
description: "Ordenar por marca",
|
||||
icon: "Building2",
|
||||
},
|
||||
{
|
||||
value: "energy",
|
||||
label: "Calorias",
|
||||
description: "Menor valor energético primeiro",
|
||||
icon: "Flame",
|
||||
},
|
||||
{
|
||||
value: "fat",
|
||||
label: "Gorduras",
|
||||
description: "Menor teor de gordura primeiro",
|
||||
icon: "Droplet",
|
||||
},
|
||||
{
|
||||
value: "sugars",
|
||||
label: "Açúcares",
|
||||
description: "Menor teor de açúcar primeiro",
|
||||
icon: "Candy",
|
||||
},
|
||||
{
|
||||
value: "completeness",
|
||||
label: "Completude",
|
||||
description: "Produtos com mais informações primeiro",
|
||||
icon: "FileCheck",
|
||||
},
|
||||
{
|
||||
value: "with_images",
|
||||
label: "Com Fotos",
|
||||
description: "Produtos com imagens primeiro",
|
||||
icon: "Image",
|
||||
},
|
||||
{
|
||||
value: "with_nutrition",
|
||||
label: "Com Info Nutricional",
|
||||
description: "Produtos com dados nutricionais completos",
|
||||
icon: "Activity",
|
||||
},
|
||||
{
|
||||
value: "eco_score",
|
||||
label: "Eco-Score",
|
||||
description: "Melhor impacto ambiental primeiro",
|
||||
icon: "Leaf",
|
||||
},
|
||||
{
|
||||
value: "nova_group",
|
||||
label: "Nível de Processamento",
|
||||
description: "Menos processados primeiro",
|
||||
icon: "Layers",
|
||||
},
|
||||
];
|
||||
|
||||
// Categorias populares para filtros
|
||||
export const POPULAR_CATEGORIES = [
|
||||
"Beverages",
|
||||
"Dairy",
|
||||
"Snacks",
|
||||
"Bakery",
|
||||
"Meat",
|
||||
"Fruits",
|
||||
"Vegetables",
|
||||
"Cereals",
|
||||
"Sweets",
|
||||
"Condiments",
|
||||
"Frozen foods",
|
||||
"Canned foods",
|
||||
];
|
||||
|
||||
// Categorias com ícones para interface
|
||||
export const POPULAR_CATEGORIES_WITH_ICONS = [
|
||||
{ value: "Beverages", label: "Bebidas", icon: "Coffee" },
|
||||
{ value: "Dairy", label: "Laticínios", icon: "Milk" },
|
||||
{ value: "Snacks", label: "Snacks", icon: "Cookie" },
|
||||
{ value: "Bakery", label: "Padaria", icon: "Croissant" },
|
||||
{ value: "Meat", label: "Carnes", icon: "Beef" },
|
||||
{ value: "Fruits", label: "Frutas", icon: "Apple" },
|
||||
{ value: "Vegetables", label: "Vegetais", icon: "Carrot" },
|
||||
{ value: "Cereals", label: "Cereais", icon: "Wheat" },
|
||||
{ value: "Sweets", label: "Doces", icon: "Candy" },
|
||||
{ value: "Condiments", label: "Condimentos", icon: "Soup" },
|
||||
{ value: "Frozen foods", label: "Congelados", icon: "Snowflake" },
|
||||
{ value: "Canned foods", label: "Enlatados", icon: "Package" },
|
||||
];
|
||||
|
||||
// Alérgenos comuns
|
||||
export const COMMON_ALLERGENS = [
|
||||
"gluten",
|
||||
"milk",
|
||||
"eggs",
|
||||
"nuts",
|
||||
"peanuts",
|
||||
"soy",
|
||||
"fish",
|
||||
"shellfish",
|
||||
"sesame",
|
||||
];
|
||||
|
||||
// Aditivos comuns
|
||||
export const COMMON_ADDITIVES = [
|
||||
"preservatives",
|
||||
"artificial colors",
|
||||
"artificial flavors",
|
||||
"sweeteners",
|
||||
"emulsifiers",
|
||||
"stabilizers",
|
||||
"thickeners",
|
||||
];
|
||||
Reference in new issue
Block a user