1272 lines
47 KiB
TypeScript
1272 lines
47 KiB
TypeScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Product, OrderItem } from "../types";
|
|
import FilterSidebar from "../components/FilterSidebar";
|
|
import Baldinho from "../components/Baldinho";
|
|
import ProductDetailModal from "../components/ProductDetailModal";
|
|
import ProductCard from "../components/ProductCard";
|
|
import ProductListItem from "../components/ProductListItem";
|
|
import SearchInput from "../components/SearchInput";
|
|
import CategoryCard from "../components/CategoryCard";
|
|
import LoadingSpinner from "../components/LoadingSpinner";
|
|
import NoImagePlaceholder from "../components/NoImagePlaceholder";
|
|
import NoData from "../components/NoData";
|
|
import {
|
|
productService,
|
|
SaleProduct,
|
|
FilterProduct,
|
|
} from "../src/services/product.service";
|
|
import { authService } from "../src/services/auth.service";
|
|
|
|
interface ProductSearchViewProps {
|
|
onAddToCart: (p: Product | OrderItem) => void;
|
|
}
|
|
|
|
const ProductSearchView: React.FC<ProductSearchViewProps> = ({
|
|
onAddToCart,
|
|
}) => {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [showResults, setShowResults] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [searchResults, setSearchResults] = useState<Product[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedDeliveryDate, setSelectedDeliveryDate] = useState("");
|
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
|
const [initialQuantity, setInitialQuantity] = useState(1);
|
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); // Modo de visualização
|
|
const [productQuantities, setProductQuantities] = useState<
|
|
Record<string, number>
|
|
>({}); // Quantidades por produto no modo lista
|
|
const [isFiltersOpen, setIsFiltersOpen] = useState(false); // Estado para drawer de filtros em mobile
|
|
|
|
// Inicializar quantidades quando os produtos mudam
|
|
useEffect(() => {
|
|
setProductQuantities((prev) => {
|
|
const newQuantities = { ...prev };
|
|
searchResults.forEach((product) => {
|
|
if (!newQuantities[product.id]) {
|
|
newQuantities[product.id] = 1;
|
|
}
|
|
});
|
|
return newQuantities;
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [searchResults.length, searchResults.map((p) => p.id).join(",")]);
|
|
|
|
// Estados para filtros
|
|
const [selectedDepartment, setSelectedDepartment] = useState("");
|
|
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
|
const [availableBrands, setAvailableBrands] = useState<string[]>([]); // Marcas disponíveis extraídas dos produtos
|
|
const [filters, setFilters] = useState({
|
|
onlyInStock: false,
|
|
stockBranch: "",
|
|
onPromotion: false,
|
|
discountRange: [0, 100] as [number, number],
|
|
priceDropped: false,
|
|
opportunities: false,
|
|
unmissableOffers: false,
|
|
});
|
|
|
|
/**
|
|
* Converte SaleProduct da API para Product do componente
|
|
* Segue o mesmo padrão do Angular
|
|
*/
|
|
const mapSaleProductToProduct = (saleProduct: SaleProduct): Product => {
|
|
// Garantir que temos pelo menos um ID válido
|
|
const productId =
|
|
saleProduct.idProduct?.toString() || saleProduct.id?.toString() || "";
|
|
if (!productId) {
|
|
console.warn("Produto sem ID válido:", saleProduct);
|
|
}
|
|
|
|
// Calcular desconto se houver diferença entre preço original e promocional
|
|
let discount: number | undefined = saleProduct.offPercent;
|
|
const listPrice = saleProduct.listPrice || 0;
|
|
const salePrice = saleProduct.salePrice || saleProduct.salePromotion || 0;
|
|
if (!discount && listPrice > 0 && salePrice > 0 && salePrice < listPrice) {
|
|
discount = Math.round(((listPrice - salePrice) / listPrice) * 100);
|
|
}
|
|
|
|
return {
|
|
id: productId || `product-${Math.random()}`,
|
|
code: productId || "",
|
|
name:
|
|
saleProduct.title ||
|
|
saleProduct.smallDescription ||
|
|
saleProduct.description ||
|
|
"Produto sem nome",
|
|
description:
|
|
saleProduct.description || saleProduct.smallDescription || undefined,
|
|
price: salePrice || listPrice || 0,
|
|
originalPrice:
|
|
listPrice > 0 && salePrice > 0 && salePrice < listPrice
|
|
? listPrice
|
|
: undefined,
|
|
discount: discount,
|
|
mark: saleProduct.brand || "Sem marca",
|
|
image: saleProduct.urlImage || "",
|
|
stockLocal: saleProduct.store_stock || saleProduct.stock || 0,
|
|
stockAvailable: saleProduct.stock || saleProduct.store_stock || 0,
|
|
stockGeneral: saleProduct.full_stock || saleProduct.stock || 0,
|
|
ean: saleProduct.ean || undefined,
|
|
model: saleProduct.idProduct?.toString() || undefined,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Busca produtos por departamento/categoria
|
|
* Segue o padrão do Angular: getProductByFilter com POST e urlCategory no body
|
|
* Request: POST /sales/products
|
|
* Body: { urlCategory: "ferragens/cadeados", brands: [], promotion: false, ... }
|
|
*/
|
|
const handleDepartmentChange = useCallback(async (urlDepartment: string) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true); // Mostrar resultados ao clicar em departamento
|
|
setSearchTerm(""); // Limpar termo de busca
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
// Criar FilterProduct seguindo o padrão do Angular
|
|
// No Angular: { brands: [], text: null, promotion: true, markdown: false, oportunity: false, offers: false, urlCategory: "ferragens/cadeados" }
|
|
const filterProduct = {
|
|
brands: [] as string[],
|
|
text: null as string | null,
|
|
promotion: false,
|
|
markdown: false,
|
|
oportunity: false,
|
|
offers: false,
|
|
urlCategory: urlDepartment, // URL do departamento/categoria
|
|
};
|
|
|
|
// Buscar produtos usando POST /sales/products com urlCategory no body
|
|
// Segue exatamente o padrão do Angular: getProductByFilter(store, page, size, filterProduct)
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1, // page
|
|
100, // size
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
// Converter SaleProduct[] para Product[]
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
// Extrair marcas dos produtos retornados (seguindo padrão do Angular)
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
// Ordenar como no Angular: remover '#' e ordenar alfabeticamente
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
|
|
// Atualizar marcas disponíveis
|
|
setAvailableBrands(uniqueBrands);
|
|
} catch (err: any) {
|
|
console.error("Erro ao buscar produtos por departamento:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Aplica os filtros selecionados e busca produtos
|
|
* Segue EXATAMENTE o padrão do Angular: getFilterProducts()
|
|
* No Angular (linha 739-781):
|
|
* 1. Atualiza this.filters com os valores dos checkboxes
|
|
* 2. Limpa produtos atuais
|
|
* 3. Chama getProductByFilter com os filtros
|
|
* 4. Extrai marcas dos produtos retornados
|
|
*/
|
|
const handleApplyFilters = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true); // Mostrar resultados ao aplicar filtros
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
// Construir FilterProduct seguindo EXATAMENTE o padrão do Angular
|
|
// No Angular (linha 740-746):
|
|
// this.filters.promotion = this.productPromotion;
|
|
// this.filters.markdown = this.markdown;
|
|
// this.filters.oportunity = this.oportunity;
|
|
// this.filters.offers = this.offers;
|
|
// this.filters.onlyWithStock = this.onlyWithStock;
|
|
// this.filters.percentOffMin = this.percentOff[0];
|
|
// this.filters.percentOffMax = this.percentOff[1];
|
|
const filterProduct: FilterProduct = {
|
|
brands: selectedBrands.length > 0 ? selectedBrands : [],
|
|
text: searchTerm.trim() || null,
|
|
urlCategory: selectedDepartment || null,
|
|
promotion: filters.onPromotion,
|
|
markdown: filters.priceDropped,
|
|
oportunity: filters.opportunities,
|
|
offers: filters.unmissableOffers,
|
|
onlyWithStock: filters.onlyInStock,
|
|
storeStock: filters.stockBranch || null,
|
|
percentOffMin: filters.discountRange[0],
|
|
percentOffMax: filters.discountRange[1],
|
|
};
|
|
|
|
// Limpar produtos atuais (seguindo padrão do Angular: linha 747-748)
|
|
setSearchResults([]);
|
|
|
|
// Buscar produtos usando POST /sales/products com filtros no body
|
|
// Segue exatamente o padrão do Angular: getProductByFilter(store, page, size, filterProduct)
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1, // page
|
|
100, // size
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
// Converter SaleProduct[] para Product[]
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
// Extrair marcas dos produtos retornados (seguindo padrão do Angular: linha 760-779)
|
|
// No Angular: ordena produtos por marca e extrai marcas únicas
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
// Ordenar como no Angular: remover '#' e ordenar alfabeticamente
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
|
|
// Atualizar marcas disponíveis (seguindo padrão do Angular: linha 760)
|
|
// No Angular: this.brands = []; e depois popula com os produtos
|
|
setAvailableBrands(uniqueBrands);
|
|
|
|
// Limpar seleção de marcas quando novos produtos são carregados
|
|
// (opcional, seguindo padrão do Angular onde marcas são resetadas)
|
|
// setSelectedBrands([]);
|
|
} catch (err: any) {
|
|
console.error("Erro ao aplicar filtros:", err);
|
|
setError(err.message || "Erro ao aplicar filtros. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [
|
|
selectedDepartment,
|
|
selectedBrands,
|
|
filters,
|
|
searchTerm,
|
|
mapSaleProductToProduct,
|
|
]);
|
|
|
|
/**
|
|
* Filtra produtos por promoção ("DE" / "POR")
|
|
* Segue o padrão do Angular: filterPromotion()
|
|
* No Angular: define promotion: true e os outros como false, depois navega para product-list
|
|
*/
|
|
const handleFilterPromotion = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true);
|
|
setSearchTerm(""); // Limpar termo de busca
|
|
setSelectedDepartment(""); // Limpar departamento selecionado
|
|
setSelectedBrands([]); // Limpar marcas selecionadas
|
|
|
|
// Atualizar filtros: promotion = true, outros = false
|
|
setFilters({
|
|
onlyInStock: false,
|
|
stockBranch: "",
|
|
onPromotion: true, // "DE" / "POR"
|
|
discountRange: [0, 100],
|
|
priceDropped: false,
|
|
opportunities: false,
|
|
unmissableOffers: false,
|
|
});
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
// Construir FilterProduct com promotion: true
|
|
const filterProduct: FilterProduct = {
|
|
brands: [],
|
|
text: null,
|
|
urlCategory: null,
|
|
promotion: true, // "DE" / "POR"
|
|
markdown: false,
|
|
oportunity: false,
|
|
offers: false,
|
|
onlyWithStock: false,
|
|
storeStock: null,
|
|
percentOffMin: 0,
|
|
percentOffMax: 100,
|
|
};
|
|
|
|
setSearchResults([]);
|
|
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1,
|
|
100,
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null); // Não é um erro, apenas estado vazio
|
|
return;
|
|
}
|
|
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
// Extrair marcas
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
setAvailableBrands(uniqueBrands);
|
|
} catch (err: any) {
|
|
console.error("Erro ao filtrar produtos em promoção:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [mapSaleProductToProduct]);
|
|
|
|
/**
|
|
* Filtra produtos que baixaram de preço
|
|
* Segue o padrão do Angular: filterMarkdown()
|
|
*/
|
|
const handleFilterMarkdown = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true);
|
|
setSearchTerm("");
|
|
setSelectedDepartment("");
|
|
setSelectedBrands([]);
|
|
|
|
setFilters({
|
|
onlyInStock: false,
|
|
stockBranch: "",
|
|
onPromotion: false,
|
|
discountRange: [0, 100],
|
|
priceDropped: true, // BAIXARAM DE PREÇO
|
|
opportunities: false,
|
|
unmissableOffers: false,
|
|
});
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
const filterProduct: FilterProduct = {
|
|
brands: [],
|
|
text: null,
|
|
urlCategory: null,
|
|
promotion: false,
|
|
markdown: true, // BAIXARAM DE PREÇO
|
|
oportunity: false,
|
|
offers: false,
|
|
onlyWithStock: false,
|
|
storeStock: null,
|
|
percentOffMin: 0,
|
|
percentOffMax: 100,
|
|
};
|
|
|
|
setSearchResults([]);
|
|
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1,
|
|
100,
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
setAvailableBrands(uniqueBrands);
|
|
} catch (err: any) {
|
|
console.error("Erro ao filtrar produtos que baixaram de preço:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [mapSaleProductToProduct]);
|
|
|
|
/**
|
|
* Filtra produtos em oportunidade
|
|
* Segue o padrão do Angular: filterOportunity()
|
|
*/
|
|
const handleFilterOpportunity = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true);
|
|
setSearchTerm("");
|
|
setSelectedDepartment("");
|
|
setSelectedBrands([]);
|
|
|
|
setFilters({
|
|
onlyInStock: false,
|
|
stockBranch: "",
|
|
onPromotion: false,
|
|
discountRange: [0, 100],
|
|
priceDropped: false,
|
|
opportunities: true, // OPORTUNIDADE
|
|
unmissableOffers: false,
|
|
});
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
const filterProduct: FilterProduct = {
|
|
brands: [],
|
|
text: null,
|
|
urlCategory: null,
|
|
promotion: false,
|
|
markdown: false,
|
|
oportunity: true, // OPORTUNIDADE
|
|
offers: false,
|
|
onlyWithStock: false,
|
|
storeStock: null,
|
|
percentOffMin: 0,
|
|
percentOffMax: 100,
|
|
};
|
|
|
|
setSearchResults([]);
|
|
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1,
|
|
100,
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
setAvailableBrands(uniqueBrands);
|
|
} catch (err: any) {
|
|
console.error("Erro ao filtrar oportunidades:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [mapSaleProductToProduct]);
|
|
|
|
/**
|
|
* Filtra ofertas imperdíveis
|
|
* Segue o padrão do Angular: filterOffers()
|
|
*/
|
|
const handleFilterOffers = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(true);
|
|
setSearchTerm("");
|
|
setSelectedDepartment("");
|
|
setSelectedBrands([]);
|
|
|
|
setFilters({
|
|
onlyInStock: false,
|
|
stockBranch: "",
|
|
onPromotion: false,
|
|
discountRange: [0, 100],
|
|
priceDropped: false,
|
|
opportunities: false,
|
|
unmissableOffers: true, // OFERTAS IMPERDÍVEIS
|
|
});
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
const filterProduct: FilterProduct = {
|
|
brands: [],
|
|
text: null,
|
|
urlCategory: null,
|
|
promotion: false,
|
|
markdown: false,
|
|
oportunity: false,
|
|
offers: true, // OFERTAS IMPERDÍVEIS
|
|
onlyWithStock: false,
|
|
storeStock: null,
|
|
percentOffMin: 0,
|
|
percentOffMax: 100,
|
|
};
|
|
|
|
setSearchResults([]);
|
|
|
|
const saleProducts = await productService.getProductByFilter(
|
|
store,
|
|
1,
|
|
100,
|
|
filterProduct
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
|
|
const uniqueBrands = Array.from(
|
|
new Set(
|
|
mappedProducts
|
|
.map((p) => p.mark)
|
|
.filter((b) => b && b !== "Sem marca")
|
|
)
|
|
).sort((a, b) => {
|
|
const brandA = a.replace("#", "").toLowerCase();
|
|
const brandB = b.replace("#", "").toLowerCase();
|
|
return brandA < brandB ? -1 : brandA > brandB ? 1 : 0;
|
|
});
|
|
setAvailableBrands(uniqueBrands);
|
|
} catch (err: any) {
|
|
console.error("Erro ao filtrar ofertas imperdíveis:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [mapSaleProductToProduct]);
|
|
|
|
const categories = [
|
|
{
|
|
label: '"DE" / "POR"',
|
|
icon: (
|
|
<div className="flex flex-col items-center">
|
|
<div className="w-12 h-12 rounded-full border-4 border-orange-500 flex items-center justify-center font-black text-orange-600 text-2xl bg-white shadow-inner">
|
|
$
|
|
</div>
|
|
<svg
|
|
className="w-8 h-8 text-orange-500 -mt-2 drop-shadow-sm"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
),
|
|
onClick: handleFilterPromotion,
|
|
},
|
|
{
|
|
label: "BAIXARAM DE PREÇO",
|
|
icon: (
|
|
<div className="relative">
|
|
<svg
|
|
className="w-16 h-16 text-orange-500 drop-shadow-md"
|
|
fill="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path d="M12.76 2.24A2 2 0 0 0 11.34 2H4a2 2 0 0 0-2 2v7.34a2 2 0 0 0 .59 1.42l9.41 9.41a2 2 0 0 0 2.83 0l7.34-7.34a2 2 0 0 0 0-2.83l-9.41-9.41zM7 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm4.24 6l1.42 1.42L9.42 19.66 8 18.24l3.24-3.24zM16 11l-1.42-1.42 3.24-3.24L19.24 7.76 16 11z" />
|
|
</svg>
|
|
</div>
|
|
),
|
|
onClick: handleFilterMarkdown,
|
|
},
|
|
{
|
|
label: "OPORTUNIDADE",
|
|
icon: (
|
|
<div className="text-7xl drop-shadow-xl animate-pulse-slow">🤑</div>
|
|
),
|
|
onClick: handleFilterOpportunity,
|
|
},
|
|
{
|
|
label: "OFERTAS IMPERDÍVEIS",
|
|
icon: (
|
|
<div className="p-4 bg-orange-50 rounded-3xl group-hover:bg-orange-100 transition-colors">
|
|
<svg
|
|
className="w-14 h-14 text-orange-600 drop-shadow-sm"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
|
|
</svg>
|
|
</div>
|
|
),
|
|
onClick: handleFilterOffers,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Realiza a busca de produtos por termo de pesquisa
|
|
* Segue o mesmo padrão do Angular: searchProduct(store, page, size, search)
|
|
*/
|
|
const handleSearch = useCallback(async () => {
|
|
const trimmedSearch = searchTerm.trim();
|
|
|
|
// Validação: mínimo de 3 caracteres (seguindo padrão do Angular)
|
|
if (trimmedSearch.length < 3) {
|
|
setError("Digite pelo menos 3 caracteres para buscar");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setShowResults(false);
|
|
|
|
const store = authService.getStore();
|
|
if (!store) {
|
|
throw new Error("Loja não encontrada. Faça login novamente.");
|
|
}
|
|
|
|
// Buscar produtos usando o método searchProduct (GET /sales/products/{search})
|
|
// Seguindo o padrão do Angular: searchProduct(store, page, size, search)
|
|
const saleProducts = await productService.searchProduct(
|
|
store,
|
|
1, // page
|
|
100, // size (no Angular usa 50 ou 5000 dependendo do contexto)
|
|
trimmedSearch.toUpperCase() // No Angular converte para uppercase
|
|
);
|
|
|
|
if (!Array.isArray(saleProducts) || saleProducts.length === 0) {
|
|
setSearchResults([]);
|
|
setError(null);
|
|
setShowResults(true);
|
|
return;
|
|
}
|
|
|
|
// Converter SaleProduct[] para Product[]
|
|
const mappedProducts = saleProducts.map(mapSaleProductToProduct);
|
|
setSearchResults(mappedProducts);
|
|
setShowResults(true);
|
|
} catch (err: any) {
|
|
console.error("Erro ao buscar produtos:", err);
|
|
setError(err.message || "Erro ao buscar produtos. Tente novamente.");
|
|
setSearchResults([]);
|
|
setShowResults(true);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [searchTerm]);
|
|
|
|
return (
|
|
<div className="h-full flex bg-[#f8fafc] relative">
|
|
{/* FilterSidebar - Desktop (sidebar fixa) */}
|
|
<div className="hidden xl:block">
|
|
<FilterSidebar
|
|
selectedDepartment={selectedDepartment}
|
|
onDepartmentChange={(url) => {
|
|
setSelectedDepartment(url);
|
|
handleDepartmentChange(url);
|
|
}}
|
|
selectedBrands={selectedBrands}
|
|
onBrandsChange={setSelectedBrands}
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
onApplyFilters={handleApplyFilters}
|
|
brands={availableBrands}
|
|
/>
|
|
</div>
|
|
|
|
{/* Overlay para mobile quando filtros estão abertos */}
|
|
{isFiltersOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-40 xl:hidden"
|
|
onClick={() => setIsFiltersOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* FilterSidebar - Mobile/Tablet (drawer) */}
|
|
<div
|
|
className={`fixed xl:hidden inset-y-0 left-0 z-50 w-full max-w-sm bg-white border-r border-slate-200 flex flex-col overflow-hidden transform transition-transform duration-300 ease-out ${
|
|
isFiltersOpen ? "translate-x-0" : "-translate-x-full"
|
|
}`}
|
|
>
|
|
<div className="p-safe-top p-4 border-b border-slate-200 flex items-center justify-between bg-[#002147] text-white flex-shrink-0">
|
|
<h3 className="text-lg font-black">Filtros</h3>
|
|
<button
|
|
onClick={() => setIsFiltersOpen(false)}
|
|
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors touch-manipulation"
|
|
title="Fechar filtros"
|
|
>
|
|
<svg
|
|
className="w-6 h-6"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-safe-bottom">
|
|
<FilterSidebar
|
|
selectedDepartment={selectedDepartment}
|
|
onDepartmentChange={(url) => {
|
|
setSelectedDepartment(url);
|
|
handleDepartmentChange(url);
|
|
setIsFiltersOpen(false); // Fechar drawer após seleção
|
|
}}
|
|
selectedBrands={selectedBrands}
|
|
onBrandsChange={setSelectedBrands}
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
onApplyFilters={() => {
|
|
handleApplyFilters();
|
|
setIsFiltersOpen(false); // Fechar drawer após aplicar filtros
|
|
}}
|
|
brands={availableBrands}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="flex-1 flex flex-col overflow-hidden h-full">
|
|
{!showResults ? (
|
|
<div className="flex-1 p-4 lg:p-10 overflow-auto custom-scrollbar h-full">
|
|
<div className="max-w-[1400px] mx-auto py-4 lg:py-8">
|
|
{/* Barra superior com botão de filtros e input de busca - Mobile/Tablet */}
|
|
<div className="lg:hidden mb-6 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => setIsFiltersOpen(true)}
|
|
className="p-3 bg-white rounded-xl shadow-sm border border-slate-200 text-slate-600 hover:bg-slate-50 transition-all touch-manipulation flex items-center justify-center"
|
|
title="Filtros"
|
|
>
|
|
<svg
|
|
className="w-6 h-6"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<div className="flex-1">
|
|
<SearchInput
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
onSearch={handleSearch}
|
|
loading={loading}
|
|
placeholder="Ex: Cimento, Tijolo, Furadeira..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop: Título e input centralizado */}
|
|
<div className="hidden lg:block text-center mb-10">
|
|
<h2 className="text-3xl font-black text-[#002147] mb-2 tracking-tight">
|
|
O que vamos vender hoje?
|
|
</h2>
|
|
<p className="text-slate-500 text-sm font-medium mb-6">
|
|
Pesquise por nome, código ou categoria do produto.
|
|
</p>
|
|
<div className="max-w-2xl mx-auto">
|
|
<SearchInput
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
onSearch={handleSearch}
|
|
loading={loading}
|
|
placeholder="Ex: Cimento, Tijolo, Furadeira..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4 ml-1 flex items-center justify-between">
|
|
<h3 className="text-base font-black text-[#002147] uppercase tracking-tight">
|
|
Categorias em Destaque
|
|
</h3>
|
|
<span className="text-[10px] font-black text-orange-500 uppercase tracking-widest bg-orange-50 px-3 py-1 rounded-full border border-orange-100 animate-pulse">
|
|
Preços baixos hoje
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 lg:gap-4 mb-8 lg:mb-12">
|
|
{categories.map((item, idx) => (
|
|
<CategoryCard
|
|
key={idx}
|
|
label={item.label}
|
|
icon={item.icon}
|
|
onClick={item.onClick}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Componente "Baldinho" - Versão Evoluída (Painel de Logística) */}
|
|
<Baldinho
|
|
selectedDeliveryDate={selectedDeliveryDate}
|
|
onDateChange={setSelectedDeliveryDate}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col h-full w-full max-w-7xl mx-auto overflow-hidden flex-1">
|
|
{/* Header Fixo */}
|
|
<header className="flex-shrink-0 p-4 lg:p-10 pb-4 bg-[#f8fafc] border-b border-slate-200 w-full">
|
|
<div className="flex flex-col lg:flex-row justify-between items-start max-w-7xl mx-auto gap-4">
|
|
<div className="flex-1 w-full">
|
|
{/* Mobile/Tablet: Barra superior com botão de filtros, voltar e input */}
|
|
<div className="lg:hidden space-y-3 mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => {
|
|
setShowResults(false);
|
|
setSearchResults([]);
|
|
setError(null);
|
|
setSearchTerm("");
|
|
setSelectedDepartment("");
|
|
}}
|
|
className="p-2.5 text-slate-600 hover:bg-slate-100 rounded-lg transition-colors touch-manipulation"
|
|
title="Voltar"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => setIsFiltersOpen(true)}
|
|
className="p-2.5 bg-white rounded-lg shadow-sm border border-slate-200 text-slate-600 hover:bg-slate-50 transition-all touch-manipulation flex items-center justify-center"
|
|
title="Filtros"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<div className="flex-1">
|
|
<SearchInput
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
onSearch={handleSearch}
|
|
loading={loading}
|
|
placeholder="Ex: Cimento, Tijolo, Furadeira..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop: Layout original */}
|
|
<div className="hidden lg:block">
|
|
<button
|
|
onClick={() => {
|
|
setShowResults(false);
|
|
setSearchResults([]);
|
|
setError(null);
|
|
setSearchTerm("");
|
|
setSelectedDepartment("");
|
|
}}
|
|
className="text-slate-400 hover:text-slate-600 flex items-center mb-2 font-bold text-sm"
|
|
>
|
|
<svg
|
|
className="w-4 h-4 mr-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
Voltar para busca
|
|
</button>
|
|
<h2 className="text-2xl font-black text-[#002147]">
|
|
{searchTerm
|
|
? `Resultados para "${searchTerm}"`
|
|
: filters.onPromotion
|
|
? 'Produtos "DE" / "POR"'
|
|
: filters.priceDropped
|
|
? "Produtos que Baixaram de Preço"
|
|
: filters.opportunities
|
|
? "Oportunidades"
|
|
: filters.unmissableOffers
|
|
? "Ofertas Imperdíveis"
|
|
: selectedDepartment
|
|
? `Produtos em "${selectedDepartment}"`
|
|
: "Produtos Encontrados"}
|
|
</h2>
|
|
{searchResults.length > 0 && (
|
|
<p className="text-sm text-slate-500 mt-1">
|
|
{searchResults.length} produto
|
|
{searchResults.length !== 1 ? "s" : ""} encontrado
|
|
{searchResults.length !== 1 ? "s" : ""}
|
|
</p>
|
|
)}
|
|
|
|
{/* Input de busca para nova pesquisa - Desktop */}
|
|
<div className="mt-4 mb-[-50px] w-full">
|
|
<SearchInput
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
onSearch={handleSearch}
|
|
loading={loading}
|
|
placeholder="Ex: Cimento, Tijolo, Furadeira..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile/Tablet: Título dos resultados */}
|
|
<div className="lg:hidden">
|
|
<h2 className="text-lg font-black text-[#002147] mb-1">
|
|
{searchTerm
|
|
? `Resultados para "${searchTerm}"`
|
|
: filters.onPromotion
|
|
? 'Produtos "DE" / "POR"'
|
|
: filters.priceDropped
|
|
? "Produtos que Baixaram de Preço"
|
|
: filters.opportunities
|
|
? "Oportunidades"
|
|
: filters.unmissableOffers
|
|
? "Ofertas Imperdíveis"
|
|
: selectedDepartment
|
|
? `Produtos em "${selectedDepartment}"`
|
|
: "Produtos Encontrados"}
|
|
</h2>
|
|
{searchResults.length > 0 && (
|
|
<p className="text-xs text-slate-500">
|
|
{searchResults.length} produto
|
|
{searchResults.length !== 1 ? "s" : ""} encontrado
|
|
{searchResults.length !== 1 ? "s" : ""}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões de alternância de visualização */}
|
|
{searchResults.length > 0 && (
|
|
<div className="flex items-center gap-2 bg-white rounded-xl p-1 border border-slate-200 shadow-sm">
|
|
<button
|
|
onClick={() => setViewMode("grid")}
|
|
className={`p-2 rounded-lg transition-all ${
|
|
viewMode === "grid"
|
|
? "bg-[#002147] text-white shadow-md"
|
|
: "text-slate-600 hover:bg-slate-100"
|
|
}`}
|
|
title="Visualização em grade"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode("list")}
|
|
className={`p-2 rounded-lg transition-all ${
|
|
viewMode === "list"
|
|
? "bg-[#002147] text-white shadow-md"
|
|
: "text-slate-600 hover:bg-slate-100"
|
|
}`}
|
|
title="Visualização em lista"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2.5"
|
|
d="M4 6h16M4 12h16M4 18h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Área de Scroll - Apenas produtos */}
|
|
<div className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide p-4 lg:p-10 pt-4 lg:pt-6 min-h-0">
|
|
<div className="max-w-7xl mx-auto w-full lg:min-w-[600px]">
|
|
{/* Loading State */}
|
|
{loading && (
|
|
<LoadingSpinner
|
|
message="Carregando produtos..."
|
|
subMessage="Aguarde enquanto buscamos os melhores produtos para você"
|
|
/>
|
|
)}
|
|
|
|
{/* Error State - apenas erros reais (não "nenhum produto encontrado") */}
|
|
{error && !loading && (
|
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-2xl">
|
|
<p className="text-red-600 text-sm font-medium">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State - quando não há resultados e não há erro */}
|
|
{!loading && searchResults.length === 0 && !error && (
|
|
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden w-full">
|
|
<NoData
|
|
title={
|
|
filters.onPromotion
|
|
? "Nenhum produto em promoção encontrado"
|
|
: filters.priceDropped
|
|
? "Nenhum produto que baixou de preço encontrado"
|
|
: filters.opportunities
|
|
? "Nenhuma oportunidade encontrada"
|
|
: filters.unmissableOffers
|
|
? "Nenhuma oferta imperdível encontrada"
|
|
: selectedDepartment
|
|
? `Nenhum produto encontrado para "${selectedDepartment}"`
|
|
: searchTerm
|
|
? `Nenhum produto encontrado para "${searchTerm}"`
|
|
: "Nenhum produto encontrado"
|
|
}
|
|
description={
|
|
filters.onPromotion
|
|
? "Não há produtos em promoção no momento. Tente novamente mais tarde ou explore outras categorias."
|
|
: filters.priceDropped
|
|
? "Não foram encontrados produtos que baixaram de preço. Tente ajustar os filtros ou verificar em outro período."
|
|
: filters.opportunities
|
|
? "Não foram encontradas oportunidades no momento. Tente ajustar os filtros ou verificar em outro período."
|
|
: filters.unmissableOffers
|
|
? "Não foram encontradas ofertas imperdíveis no momento. Tente novamente mais tarde ou explore outras categorias."
|
|
: selectedDepartment
|
|
? `Não foram encontrados produtos para o departamento "${selectedDepartment}". Tente selecionar outro departamento ou usar termos de busca.`
|
|
: "Não foram encontrados produtos com os filtros selecionados. Tente ajustar os filtros, usar outros termos de busca ou selecione um departamento."
|
|
}
|
|
icon={
|
|
filters.onPromotion ||
|
|
filters.priceDropped ||
|
|
filters.opportunities ||
|
|
filters.unmissableOffers
|
|
? "file"
|
|
: "search"
|
|
}
|
|
variant="outline"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Results - apenas quando não está carregando e há resultados */}
|
|
{!loading && searchResults.length > 0 && (
|
|
<div
|
|
className={
|
|
viewMode === "grid"
|
|
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-6 w-full items-stretch"
|
|
: "space-y-4 w-full"
|
|
}
|
|
>
|
|
{searchResults.map((product) => (
|
|
<div
|
|
key={product.id}
|
|
className={
|
|
viewMode === "list"
|
|
? "bg-white rounded-xl border border-slate-200 p-4 hover:shadow-lg transition-all"
|
|
: "h-full"
|
|
}
|
|
>
|
|
{viewMode === "list" ? (
|
|
<ProductListItem
|
|
product={product}
|
|
quantity={productQuantities[product.id] || 1}
|
|
onQuantityChange={(newQty) => {
|
|
setProductQuantities((prev) => ({
|
|
...prev,
|
|
[product.id]: newQty,
|
|
}));
|
|
}}
|
|
onAddToCart={(
|
|
prod,
|
|
qty,
|
|
stockStore,
|
|
deliveryType
|
|
) => {
|
|
onAddToCart({
|
|
...prod,
|
|
quantity: qty,
|
|
stockStore,
|
|
deliveryType,
|
|
} as OrderItem);
|
|
}}
|
|
onViewDetails={(prod, qty) => {
|
|
setSelectedProduct(prod);
|
|
setInitialQuantity(qty || 1);
|
|
setIsDetailModalOpen(true);
|
|
}}
|
|
/>
|
|
) : (
|
|
<ProductCard
|
|
product={product}
|
|
onAddToCart={onAddToCart}
|
|
onViewDetails={(p, qty) => {
|
|
setSelectedProduct(p);
|
|
setInitialQuantity(qty || 1);
|
|
setIsDetailModalOpen(true);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
<style>{`
|
|
@keyframes pulse-slow {
|
|
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 0px orange); }
|
|
50% { transform: scale(1.05); filter: drop-shadow(0 0 10px rgba(249, 115, 22, 0.3)); }
|
|
}
|
|
.animate-pulse-slow { animation: pulse-slow 3s infinite ease-in-out; }
|
|
|
|
/* Esconder scrollbar vertical */
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
`}</style>
|
|
|
|
{/* Modal de Detalhamento do Produto */}
|
|
<ProductDetailModal
|
|
product={selectedProduct}
|
|
isOpen={isDetailModalOpen}
|
|
onClose={() => {
|
|
setIsDetailModalOpen(false);
|
|
setSelectedProduct(null);
|
|
setInitialQuantity(1);
|
|
}}
|
|
onAddToCart={onAddToCart}
|
|
initialQuantity={initialQuantity}
|
|
onProductChange={(newProduct) => {
|
|
// Atualizar o produto exibido no modal sem fechá-lo
|
|
setSelectedProduct(newProduct);
|
|
setInitialQuantity(1);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductSearchView;
|