Details now show real customer API data

This commit is contained in:
2025-08-28 12:25:39 -06:00
parent d0f691c8fd
commit 20ab14a9d2
10 changed files with 635 additions and 116 deletions

View File

@@ -2,7 +2,11 @@
"permissions": {
"allow": [
"Bash(npm run lint)",
"WebSearch"
"WebSearch",
"WebFetch(domain:www.impuls.com.mx)",
"Bash(del \"C:\\Users\\jandres\\source\\repos\\test\\my-app\\iframe-test.html\")",
"WebFetch(domain:baymard.com)",
"Bash(npx shadcn@latest add:*)"
],
"deny": [],
"ask": []

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest, { params }: { params: { productId: string } }) {
try {
const { productId } = params;
// Validate productId
if (!productId || typeof productId !== 'string') {
return NextResponse.json(
{ error: 'Product ID is required' },
{ status: 400 }
);
}
// Fetch from Impuls API
const apiUrl = `https://www.impuls.com.mx/api/catalog_system/pub/products/search/?fq=productId:${productId}`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
console.error(`API Error: ${response.status} ${response.statusText}`);
return NextResponse.json(
{ error: 'Failed to fetch product from external API' },
{ status: response.status }
);
}
const data = await response.json();
// Return first product from array or null if empty
const product = Array.isArray(data) && data.length > 0 ? data[0] : null;
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(product, {
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600'
}
});
} catch (error) {
console.error('API Route Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -20,7 +20,6 @@ export default function HomePage() {
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
const [activeShoe, setActiveShoe] = useState<Shoe | null>(null);
const [isPopupOpen, setPopupOpen] = useState(false);
const [isHistoryOpen, setHistoryOpen] = useState(false);
const [history, setHistory] = useState<Shoe[]>([]);
@@ -120,7 +119,6 @@ export default function HomePage() {
const handleScan = () => {
const detected = detectShoe(SHOE_DATABASE);
if (detected) {
setActiveShoe(detected);
const updatedHistory = addToHistory(detected);
setHistory(updatedHistory);
setPopupOpen(true);
@@ -128,7 +126,6 @@ export default function HomePage() {
};
const handleHistoryItemClick = (shoe: Shoe) => {
setActiveShoe(shoe);
setHistoryOpen(false);
setPopupOpen(true);
};
@@ -462,7 +459,7 @@ export default function HomePage() {
return (
<main className="relative h-screen w-screen bg-black overflow-hidden">
{renderContent()}
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} shoe={activeShoe} />
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} />
<HistorySidebar isOpen={isHistoryOpen} onOpenChange={setHistoryOpen} history={history} onItemClick={handleHistoryItemClick} />
</main>
);

View File

@@ -5,52 +5,108 @@ import { useState, useEffect } from 'react';
import { Drawer } from 'vaul';
import { type CarouselApi } from "@/components/ui/carousel";
import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Button } from '@/components/ui/button';
import type { Shoe } from '@/lib/shoe-database';
import { ExternalLink, Package, Truck, Shield, Star } from 'lucide-react';
import { fetchProduct, getProductImages, getProductPricing, getProductVariants, type Product } from '@/lib/product-api';
interface ShoeResultsPopupProps {
shoe: Shoe | null;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}
export default function ShoeResultsPopup({ shoe, isOpen, onOpenChange }: ShoeResultsPopupProps) {
export default function ShoeResultsPopup({ isOpen, onOpenChange }: ShoeResultsPopupProps) {
const [api, setApi] = useState<CarouselApi>();
const [activeImageUrl, setActiveImageUrl] = useState(shoe?.imageUrl);
const [allImages, setAllImages] = useState<string[]>([]);
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(false);
const [activeImageUrl, setActiveImageUrl] = useState<string>('');
const [selectedVariant, setSelectedVariant] = useState<string>('');
const [selectedSize, setSelectedSize] = useState<string>('');
// Fetch product data when popup opens
useEffect(() => {
if (shoe) {
const images = [shoe.imageUrl, ...shoe.colorOptions.map(o => o.imageUrl)].filter(Boolean) as string[];
setAllImages(images);
setActiveImageUrl(shoe.imageUrl);
api?.scrollTo(0, true); // Jump to start without animation
if (isOpen && !product) {
setLoading(true);
fetchProduct().then((data) => {
if (data) {
setProduct(data);
const images = getProductImages(data);
if (images.length > 0) {
setActiveImageUrl(images[0]);
}
// Set first available variant
const variants = getProductVariants(data);
if (variants.length > 0) {
setSelectedVariant(variants[0].itemId);
if (variants[0].sizes.length > 0) {
setSelectedSize(variants[0].sizes[0]);
}
}
}
setLoading(false);
});
}
}, [shoe, api]);
}, [isOpen, product]);
// Handle carousel selection
useEffect(() => {
if (!api) return;
if (!api || !product) return;
const handleSelect = () => {
const selectedIndex = api.selectedScrollSnap();
setActiveImageUrl(allImages[selectedIndex]);
const images = getProductImages(product);
setActiveImageUrl(images[selectedIndex]);
};
api.on("select", handleSelect);
return () => {
api.off("select", handleSelect);
};
}, [api, allImages]);
}, [api, product]);
const handleThumbnailClick = (imageUrl: string) => {
const index = allImages.findIndex(url => url === imageUrl);
if (!product) return;
const images = getProductImages(product);
const index = images.findIndex(url => url === imageUrl);
if (index !== -1) {
api?.scrollTo(index);
}
};
if (!shoe) return null;
const handleViewDetails = () => {
if (product?.linkText) {
window.open(`https://www.impuls.com.mx/${product.linkText}/p`, '_blank');
}
};
if (loading) {
return (
<Drawer.Root open={isOpen} onOpenChange={onOpenChange}>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 mt-24 flex h-[90%] flex-col rounded-t-3xl bg-black/90 backdrop-blur-xl border-t-2 border-white/20 z-[101] shadow-2xl">
<Drawer.Title className="sr-only">Cargando producto</Drawer.Title>
<Drawer.Description className="sr-only">Obteniendo información del producto</Drawer.Description>
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-4 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
<p className="text-white/80 text-lg">Cargando producto...</p>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
if (!product) return null;
const images = getProductImages(product);
const pricing = getProductPricing(product);
const variants = getProductVariants(product);
const selectedVariantData = variants.find(v => v.itemId === selectedVariant);
return (
<Drawer.Root open={isOpen} onOpenChange={onOpenChange}>
@@ -61,103 +117,166 @@ export default function ShoeResultsPopup({ shoe, isOpen, onOpenChange }: ShoeRes
<div className="sticky top-0 z-10 w-full bg-black/80 backdrop-blur-xl rounded-t-[24px] py-6 border-b border-white/10">
<div className="mx-auto h-1.5 w-16 flex-shrink-0 rounded-full bg-white/40 shadow-lg" />
</div>
<Drawer.Title className="sr-only">{shoe.name}</Drawer.Title>
<Drawer.Description className="sr-only">{shoe.description}</Drawer.Description>
<Drawer.Title className="sr-only">{product.productName}</Drawer.Title>
<Drawer.Description className="sr-only">{product.description}</Drawer.Description>
<div className="p-6 space-y-6">
{/* Image Carousel */}
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-4 border border-white/10">
<Carousel setApi={setApi} className="w-full">
<CarouselContent>
{allImages.map((url, index) => (
<CarouselItem key={index}>
<div className="relative h-72 w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-sm border border-white/20 md:h-80">
<Image
src={url}
alt={`${shoe.name} image ${index + 1}`}
layout="fill"
objectFit="contain"
className="p-4"
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
{/* Brand Badge */}
<div className="flex items-center gap-3">
<Badge variant="secondary" className="bg-white/10 text-white border-white/20">
{product.brand}
</Badge>
{pricing.discount > 0 && (
<Badge className="bg-red-500/20 text-red-300 border-red-500/30">
-{pricing.discount}%
</Badge>
)}
</div>
{/* Color Options */}
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-6 border border-white/10">
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-3">
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
Colores Disponibles
</h3>
<div className="grid grid-cols-4 gap-3">
{shoe.colorOptions.map((option) => (
<button
key={option.color}
onClick={() => handleThumbnailClick(option.imageUrl)}
className={`group relative h-20 overflow-hidden rounded-xl border-2 transition-all duration-300 hover:scale-105 ${
activeImageUrl === option.imageUrl
? 'border-blue-400 shadow-lg shadow-blue-500/25'
: 'border-white/20 hover:border-white/40'
}`}
>
<Image src={option.imageUrl} alt={option.color} layout="fill" objectFit="cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<div className="absolute bottom-1 left-1 right-1">
<p className="text-white text-xs font-medium truncate drop-shadow-lg">
{option.color}
</p>
</div>
{activeImageUrl === option.imageUrl && (
<div className="absolute top-1 right-1 w-3 h-3 bg-blue-400 rounded-full border border-white"></div>
)}
</button>
))}
</div>
</div>
{/* Product Info */}
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-6 border border-white/10">
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
<h1 className="text-3xl font-bold tracking-tight text-white md:text-4xl">
{shoe.name}
</h1>
{shoe.promotions && shoe.promotions.length > 0 && (
<div className="flex flex-wrap gap-2">
{shoe.promotions.map((promotion, index) => (
<div
key={index}
className={`px-3 py-1 rounded-full text-xs font-bold border ${
promotion.type === 'discount'
? 'bg-red-500/20 text-red-300 border-red-500/30'
: promotion.type === 'new'
? 'bg-green-500/20 text-green-300 border-green-500/30'
: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
}`}
>
{promotion.label}
</div>
))}
</div>
)}
</div>
<div className="flex items-baseline gap-4 mb-4">
<p className="text-4xl font-bold bg-gradient-to-r from-green-400 to-emerald-400 bg-clip-text text-transparent">
${shoe.price.toFixed(2)}
{/* Product Name & Price */}
<div className="space-y-2">
<h1 className="text-2xl font-bold tracking-tight text-white md:text-3xl leading-tight">
{product.productName}
</h1>
<div className="flex items-baseline gap-4">
<p className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-400 bg-clip-text text-transparent">
${pricing.price.toFixed(2)}
</p>
{shoe.promotions?.find(p => p.originalPrice) && (
<p className="text-2xl text-gray-500 line-through">
${shoe.promotions.find(p => p.originalPrice)?.originalPrice?.toFixed(2)}
{pricing.listPrice > pricing.price && (
<p className="text-xl text-gray-500 line-through">
${pricing.listPrice.toFixed(2)}
</p>
)}
</div>
<p className="text-lg text-white/80 leading-relaxed">
{shoe.description}
</p>
</div>
{/* Image Carousel */}
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-4 border border-white/10">
{images.length > 0 ? (
<Carousel setApi={setApi} className="w-full">
<CarouselContent>
{images.map((url, index) => (
<CarouselItem key={index}>
<div className="relative h-72 w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-sm border border-white/20 md:h-80">
<Image
src={url}
alt={`${product.productName} imagen ${index + 1}`}
layout="fill"
objectFit="contain"
className="p-4"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
) : (
<div className="relative h-72 w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-sm border border-white/20 md:h-80 flex items-center justify-center">
<div className="flex flex-col items-center gap-4 text-white/60">
<Package size={48} />
<p className="text-lg font-medium">Imagen no disponible</p>
</div>
</div>
)}
</div>
{/* Size Selection */}
{selectedVariantData && selectedVariantData.sizes.length > 0 && (
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-6 border border-white/10">
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-3">
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
Tallas Disponibles
</h3>
<div className="grid grid-cols-4 gap-3">
{selectedVariantData.sizes.map((size) => (
<button
key={size}
onClick={() => setSelectedSize(size)}
className={`h-12 rounded-lg border-2 transition-all duration-300 hover:scale-105 flex items-center justify-center font-medium ${
selectedSize === size
? 'border-blue-400 bg-blue-400/20 text-blue-300 shadow-lg shadow-blue-500/25'
: 'border-white/20 bg-white/5 text-white hover:border-white/40 hover:bg-white/10'
}`}
>
{size}
</button>
))}
</div>
</div>
)}
{/* Product Details Accordion */}
<div className="bg-white/5 backdrop-blur-sm rounded-2xl border border-white/10 overflow-hidden">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="description" className="border-white/10">
<AccordionTrigger className="px-6 py-4 text-white hover:no-underline hover:bg-white/5">
<div className="flex items-center gap-3">
<Star className="w-4 h-4" />
<span className="font-semibold">Descripción</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-4 text-white/80 leading-relaxed">
{product.description || 'Descripción no disponible.'}
</AccordionContent>
</AccordionItem>
<AccordionItem value="variants" className="border-white/10">
<AccordionTrigger className="px-6 py-4 text-white hover:no-underline hover:bg-white/5">
<div className="flex items-center gap-3">
<Package className="w-4 h-4" />
<span className="font-semibold">Variantes ({variants.length})</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-4">
<div className="space-y-3">
{variants.map((variant) => (
<div key={variant.itemId} className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/10">
<div className="text-white font-medium">{variant.name}</div>
<Badge variant={variant.isAvailable ? "default" : "secondary"}>
{variant.isAvailable ? 'Disponible' : 'Agotado'}
</Badge>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="shipping" className="border-white/10">
<AccordionTrigger className="px-6 py-4 text-white hover:no-underline hover:bg-white/5">
<div className="flex items-center gap-3">
<Truck className="w-4 h-4" />
<span className="font-semibold">Envío y Devoluciones</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-4 text-white/80">
<div className="space-y-2">
<p> Envío gratuito en compras mayores a $999</p>
<p> Entrega en 3-5 días hábiles</p>
<p> Devoluciones gratuitas dentro de 30 días</p>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="warranty" className="border-white/10 border-b-0">
<AccordionTrigger className="px-6 py-4 text-white hover:no-underline hover:bg-white/5">
<div className="flex items-center gap-3">
<Shield className="w-4 h-4" />
<span className="font-semibold">Garantía</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-4 text-white/80">
<div className="space-y-2">
<p> Garantía del fabricante de 6 meses</p>
<p> Protección contra defectos de manufactura</p>
<p> Soporte técnico especializado</p>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>
@@ -165,8 +284,13 @@ export default function ShoeResultsPopup({ shoe, isOpen, onOpenChange }: ShoeRes
{/* Action Button */}
<div className="sticky bottom-0 w-full border-t border-white/10 bg-black/80 backdrop-blur-xl p-6">
<div className="flex gap-3">
<Button size="lg" className="flex-1 h-14 text-lg bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 border-0 shadow-lg shadow-blue-500/25 transform hover:scale-[1.02] transition-all duration-200">
🛒 Añadir al Carrito
<Button
size="lg"
onClick={handleViewDetails}
className="flex-1 h-14 text-lg bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 border-0 shadow-lg shadow-blue-500/25 transform hover:scale-[1.02] transition-all duration-200"
>
<ExternalLink className="w-5 h-5 mr-2" />
Ver Detalles Completos
</Button>
<Button size="lg" variant="outline" className="h-14 px-6 bg-white/5 border-white/20 text-white hover:bg-white/10 hover:border-white/30">
@@ -177,4 +301,4 @@ export default function ShoeResultsPopup({ shoe, isOpen, onOpenChange }: ShoeRes
</Drawer.Portal>
</Drawer.Root>
);
}
}

View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

149
lib/product-api.ts Normal file
View File

@@ -0,0 +1,149 @@
export interface ProductImage {
imageId: string;
imageLabel: string;
imageTag: string;
imageUrl: string;
imageText: string;
imageLastModified: string;
}
export interface CommercialOffer {
Price: number;
ListPrice: number;
PriceWithoutDiscount: number;
FullSellingPrice: number;
RewardValue: number;
PriceValidUntil: string;
AvailableQuantity: number;
IsAvailable: boolean;
GiftSkuIds: string[];
Installments: {
Value: number;
InterestRate: number;
TotalValuePlusInterestRate: number;
NumberOfInstallments: number;
}[];
discountHighLight: unknown[];
teasers: unknown[];
}
export interface ProductSeller {
sellerId: string;
sellerName: string;
addToCartLink: string;
sellerDefault: boolean;
commertialOffer: CommercialOffer;
}
export interface ProductItem {
itemId: string;
name: string;
nameComplete: string;
complementName: string;
ean: string;
referenceId: { Key: string; Value: string }[];
measurementUnit: string;
unitMultiplier: number;
modalType: null;
isKit: boolean;
images: ProductImage[];
Talla: string[];
variations: string[];
sellers: ProductSeller[];
Videos: unknown[];
estimatedDateArrival: null;
}
export interface Product {
productId: string;
productName: string;
brand: string;
brandId: number;
brandImageUrl: null | string;
linkText: string;
productReference: string;
productReferenceCode: string;
categoryId: string;
productTitle: string;
metaTagDescription: string;
releaseDate: string;
clusterHighlights: Record<string, unknown>;
productClusters: Record<string, string>;
searchableClusters: Record<string, unknown>;
categories: string[];
categoriesIds: string[];
link: string;
description: string;
items: ProductItem[];
}
const FIXED_PRODUCT_ID = '169520';
export async function fetchProduct(productId: string = FIXED_PRODUCT_ID): Promise<Product | null> {
try {
// Use our Next.js API route as a proxy to avoid CORS issues
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error('Failed to fetch product:', response.status, errorData);
return null;
}
const product = await response.json();
return product;
} catch (error) {
console.error('Error fetching product:', error);
return null;
}
}
export function getProductImages(product: Product): string[] {
const allImages: string[] = [];
product.items.forEach(item => {
item.images.forEach(image => {
if (image.imageUrl && !allImages.includes(image.imageUrl)) {
allImages.push(image.imageUrl);
}
});
});
return allImages;
}
export function getProductPricing(product: Product) {
const firstAvailableItem = product.items.find(item =>
item.sellers.some(seller => seller.commertialOffer.IsAvailable)
);
if (!firstAvailableItem) {
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
}
const seller = firstAvailableItem.sellers.find(s => s.commertialOffer.IsAvailable);
if (!seller) {
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
}
const { Price, ListPrice } = seller.commertialOffer;
const discount = ListPrice > Price ? Math.round(((ListPrice - Price) / ListPrice) * 100) : 0;
return {
price: Price,
listPrice: ListPrice,
discount,
isAvailable: true
};
}
export function getProductVariants(product: Product) {
return product.items.map(item => ({
itemId: item.itemId,
name: item.name,
sizes: item.Talla,
images: item.images.map(img => img.imageUrl),
isAvailable: item.sellers.some(seller => seller.commertialOffer.IsAvailable),
price: item.sellers.find(s => s.commertialOffer.IsAvailable)?.commertialOffer.Price || 0
}));
}

View File

@@ -4,10 +4,13 @@ const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https'
,
protocol: 'https',
hostname: 'placehold.co',
},
{
protocol: 'https',
hostname: 'b2cimpulsmx.vteximg.com.br',
},
],
},
};

86
package-lock.json generated
View File

@@ -8,8 +8,10 @@
"name": "my-app",
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -966,6 +968,37 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -989,6 +1022,36 @@
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -1327,6 +1390,29 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",

View File

@@ -9,8 +9,10 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",