| import { |
| type PointerEvent, |
| type SyntheticEvent, |
| type RefObject, |
| } from "react"; |
| import { |
| ArrowLeft, |
| Share2, |
| Download, |
| MapPin, |
| ShoppingCart, |
| RefreshCw, |
| RotateCcw, |
| Paintbrush, |
| Loader2, |
| } from "lucide-react"; |
| import type { Product } from "../../types"; |
| import type { SegmentMeta } from "../../hooks/useSegmentCanvas"; |
|
|
| interface RoomPreviewPanelProps { |
| previewImage?: string | null; |
| offset: { x: number; y: number }; |
| zoom: number; |
| imageSize: { width: number; height: number }; |
| wrapperRef: RefObject<HTMLDivElement | null>; |
| canvasRef: RefObject<HTMLCanvasElement | null>; |
| selectedProduct: Product | null; |
| selectedMasks: Set<number>; |
| hoveredMask: number; |
| segmentMeta: Map<number, SegmentMeta>; |
| isApplying: boolean; |
| onBack: () => void; |
| onPointerDown: (event: PointerEvent<HTMLDivElement>) => void; |
| onPointerMove: (event: PointerEvent<HTMLDivElement>) => void; |
| onPointerUp: (event: PointerEvent<HTMLDivElement>) => void; |
| updateImageSize: (img: HTMLImageElement) => void; |
| onCanvasMouseMove: (e: React.MouseEvent<HTMLCanvasElement>) => void; |
| onCanvasMouseLeave: () => void; |
| onCanvasClick: (e: React.MouseEvent<HTMLCanvasElement>) => void; |
| onApplyTexture: () => void; |
| onReset: () => void; |
| onDownload: () => Promise<void>; |
| onShare: () => Promise<void>; |
| } |
|
|
| export function RoomPreviewPanel({ |
| previewImage, |
| offset, |
| zoom, |
| imageSize, |
| wrapperRef, |
| canvasRef, |
| selectedProduct, |
| selectedMasks, |
| hoveredMask, |
| segmentMeta, |
| isApplying, |
| onBack, |
| onPointerDown, |
| onPointerMove, |
| onPointerUp, |
| updateImageSize, |
| onCanvasMouseMove, |
| onCanvasMouseLeave, |
| onCanvasClick, |
| onApplyTexture, |
| onReset, |
| onDownload, |
| onShare, |
| }: RoomPreviewPanelProps) { |
| const canApply = selectedMasks.size > 0 && selectedProduct != null; |
|
|
| const getLabel = (index: number) => |
| segmentMeta.get(index)?.label ?? `Zona ${index}`; |
|
|
| return ( |
| <div className="w-full h-full bg-white overflow-hidden"> |
| <div className="relative h-full overflow-hidden lg:rounded-lg bg-[#f4f8ff]"> |
| |
| {/* ββ Mobile top bar βββββββββββββββββββββββββββββββββββββββββ */} |
| {/* pr-12 reserva espacio a la derecha para el botΓ³n .hr-close del padre */} |
| <div className="lg:hidden absolute left-0 right-0 top-0 z-10 h-12 bg-white shadow-sm flex items-center justify-between px-3 pr-12"> |
| <button |
| onClick={onBack} |
| className="p-2 rounded-full hover:bg-gray-100 transition-colors" |
| > |
| <ArrowLeft className="h-5 w-5 text-[#333]" /> |
| </button> |
| <div className="flex items-center gap-1"> |
| <button |
| onClick={onShare} |
| className="p-2 rounded-full hover:bg-[#eaf1ff] transition-colors" |
| > |
| <Share2 className="h-5 w-5 text-[#0047AB]" /> |
| </button> |
| <button |
| onClick={onDownload} |
| className="p-2 rounded-full hover:bg-[#eaf1ff] transition-colors" |
| > |
| <Download className="h-5 w-5 text-[#0047AB]" /> |
| </button> |
| </div> |
| </div> |
| |
| {/* ββ Desktop top bar ββββββββββββββββββββββββββββββββββββββββ */} |
| <div className="pointer-events-none hidden lg:flex absolute left-1/2 top-0 z-10 -translate-x-1/2 w-[80%] h-16 rounded-b-md bg-white shadow-sm items-center justify-center gap-4 px-3"> |
| <button |
| onClick={onBack} |
| className="pointer-events-auto rounded-full bg-[#333333] px-4 py-2 text-sm font-semibold text-white hover:bg-black flex items-center gap-2" |
| > |
| <ArrowLeft className="h-4 w-4" /> Cambiar de HabitaciΓ³n |
| </button> |
| <span className="text-gray-400">|</span> |
| <button |
| onClick={onShare} |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2" |
| > |
| <Share2 className="h-4 w-4 text-[#0047AB]" /> |
| Compartir |
| </button> |
| <button |
| onClick={onDownload} |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2" |
| > |
| <Download className="h-4 w-4 text-[#0047AB]" /> Descargar |
| </button> |
| <a |
| href="https://nauffargermany.com/gt/sucursales-2/" |
| target="_blank" |
| rel="noopener noreferrer" |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2" |
| > |
| <MapPin className="h-4 w-4 text-[#0047AB]" /> Encuentra tu tienda |
| </a> |
| {selectedProduct?.detailUrl ? ( |
| <a |
| href={selectedProduct.detailUrl} |
| target="_blank" |
| rel="noopener noreferrer" |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2" |
| > |
| <ShoppingCart className="h-4 w-4 text-[#0047AB]" /> Ir a la pΓ‘gina del producto |
| </a> |
| ) : ( |
| <button |
| disabled |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-gray-300 flex items-center gap-2 cursor-default" |
| > |
| <ShoppingCart className="h-4 w-4 text-gray-300" /> Ir a la pΓ‘gina del producto |
| </button> |
| )} |
| </div> |
| |
| {/* ββ Γrea de imagen + canvas ββββββββββββββββββββββββββββββββ */} |
| <div |
| ref={wrapperRef} |
| className="absolute inset-x-0 top-12 lg:top-16 bottom-12 lg:bottom-16 flex items-center justify-center bg-[#edf4ff] overflow-hidden" |
| onPointerDown={onPointerDown} |
| onPointerMove={onPointerMove} |
| onPointerUp={onPointerUp} |
| > |
| {previewImage ? ( |
| <div |
| style={{ |
| transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`, |
| transformOrigin: "center center", |
| position: "relative", |
| display: "inline-flex", |
| lineHeight: 0, |
| ...(imageSize.width > 0 |
| ? { width: imageSize.width, height: imageSize.height } |
| : {}), |
| }} |
| > |
| <img |
| src={previewImage} |
| alt="Vista previa de la habitaciΓ³n" |
| draggable={false} |
| onDragStart={(e) => e.preventDefault()} |
| onLoad={(event: SyntheticEvent<HTMLImageElement>) => |
| updateImageSize(event.currentTarget) |
| } |
| style={{ |
| display: "block", |
| width: imageSize.width > 0 ? "100%" : "auto", |
| height: imageSize.height > 0 ? "100%" : "auto", |
| maxWidth: imageSize.width > 0 ? "none" : "100%", |
| maxHeight: imageSize.height > 0 ? "none" : "100%", |
| objectFit: "contain", |
| }} |
| /> |
| <canvas |
| ref={canvasRef} |
| style={{ |
| position: "absolute", |
| inset: 0, |
| width: "100%", |
| height: "100%", |
| cursor: "crosshair", |
| }} |
| onMouseMove={onCanvasMouseMove} |
| onMouseLeave={onCanvasMouseLeave} |
| onClick={onCanvasClick} |
| /> |
| </div> |
| ) : ( |
| <div className="flex h-full w-full items-center justify-center text-[#707070] text-sm px-6 text-center"> |
| No hay vista previa disponible aΓΊn. |
| </div> |
| )} |
| </div> |
| |
| {/* ββ Hint de selecciΓ³n ββββββββββββββββββββββββββββββββββββββ */} |
| {previewImage && selectedMasks.size === 0 && ( |
| <div className="pointer-events-none absolute top-14 lg:top-20 left-1/2 -translate-x-1/2 z-10 bg-black/50 text-white text-xs px-3 py-1.5 rounded-full whitespace-nowrap"> |
| {hoveredMask > 0 |
| ? `${getLabel(hoveredMask)} β haz clic para seleccionar` |
| : "Haz clic sobre una zona de la imagen para seleccionarla"} |
| </div> |
| )} |
| |
| {/* ββ Mobile bottom bar ββββββββββββββββββββββββββββββββββββββ */} |
| <div className="pointer-events-none lg:hidden absolute left-0 right-0 bottom-0 z-10 h-12 bg-white border-t border-gray-100 shadow-sm flex items-center px-3 gap-2"> |
| {selectedProduct && ( |
| <div className="flex items-center gap-2 min-w-0 flex-1"> |
| <img |
| src={selectedProduct.image} |
| alt={selectedProduct.name} |
| className="w-8 h-8 object-cover rounded-md border border-gray-200 shrink-0" |
| /> |
| <div className="min-w-0"> |
| <p className="text-[10px] text-[#707070] truncate">{selectedProduct.brand}</p> |
| <p className="text-xs font-semibold text-[#333] truncate leading-tight">{selectedProduct.name}</p> |
| </div> |
| </div> |
| )} |
| {!selectedProduct && selectedMasks.size > 0 && ( |
| <p className="text-xs text-[#0047AB] font-medium truncate flex-1"> |
| {[...selectedMasks].map(getLabel).join(", ")} |
| </p> |
| )} |
| {!selectedProduct && selectedMasks.size === 0 && <div className="flex-1" />} |
| |
| <div className="flex items-center gap-1 ml-auto shrink-0"> |
| {canApply && ( |
| <button |
| onClick={onApplyTexture} |
| disabled={isApplying} |
| className="pointer-events-auto flex items-center gap-1.5 bg-[#0047AB] text-white px-3 py-1.5 rounded-full text-xs font-semibold hover:bg-[#003a94] disabled:opacity-60 transition-colors" |
| > |
| {isApplying ? ( |
| <Loader2 className="h-3 w-3 animate-spin" /> |
| ) : ( |
| <Paintbrush className="h-3 w-3" /> |
| )} |
| {isApplying ? "Aplicando..." : "Aplicar"} |
| </button> |
| )} |
| <button |
| onClick={onReset} |
| className="pointer-events-auto p-2 rounded-full hover:bg-[#eaf1ff] transition-colors" |
| > |
| <RefreshCw className="h-4 w-4 text-[#0047AB]" /> |
| </button> |
| </div> |
| </div> |
| |
| {/* ββ Desktop bottom bar βββββββββββββββββββββββββββββββββββββ */} |
| <div className="pointer-events-none hidden lg:flex absolute left-1/2 bottom-0 z-10 -translate-x-1/2 w-[80%] h-16 rounded-t-md bg-white border border-[#0047AB]/10 shadow-sm items-center justify-start gap-4 px-4"> |
| {selectedProduct && ( |
| <div className="pointer-events-none flex items-center gap-3"> |
| <img |
| src={selectedProduct.image} |
| alt={selectedProduct.name} |
| className="w-10 h-10 object-cover rounded-md border border-gray-200" |
| /> |
| <div> |
| <p className="text-[#707070] text-xs">{selectedProduct.brand}</p> |
| <p className="font-semibold text-[#333333] text-sm leading-tight"> |
| {selectedProduct.name} |
| </p> |
| </div> |
| </div> |
| )} |
| |
| {selectedMasks.size > 0 && ( |
| <p className="text-xs text-[#0047AB] font-medium truncate max-w-[260px]"> |
| {[...selectedMasks].map(getLabel).join(", ")} |
| </p> |
| )} |
| |
| <div className="flex-1" /> |
| |
| {canApply && ( |
| <button |
| onClick={onApplyTexture} |
| disabled={isApplying} |
| className="pointer-events-auto flex items-center gap-2 bg-[#0047AB] text-white px-4 py-2 rounded-full text-sm font-semibold hover:bg-[#003a94] disabled:opacity-60 transition-colors" |
| > |
| {isApplying ? ( |
| <Loader2 className="h-4 w-4 animate-spin" /> |
| ) : ( |
| <Paintbrush className="h-4 w-4" /> |
| )} |
| {isApplying ? "Aplicando..." : "Aplicar textura"} |
| </button> |
| )} |
| |
| <button |
| onClick={onReset} |
| className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2" |
| > |
| <span className="inline-flex items-center justify-center rounded-full bg-[#eaf1ff] p-2"> |
| <RefreshCw className="h-4 w-4 text-[#0047AB]" /> |
| </span> |
| Reiniciar |
| </button> |
| <button className="pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2"> |
| <span className="inline-flex items-center justify-center rounded-full bg-[#eaf1ff] p-2"> |
| <RotateCcw className="h-4 w-4 text-[#0047AB]" /> |
| </span> |
| Girar |
| </button> |
| </div> |
| |
| </div> |
| </div> |
| ); |
| } |
| |