- {/* Selector Suelos / Paredes */}
-
-
-
-
-
{/* Barra de herramientas */}
@@ -328,7 +380,6 @@ export default function RoomVisualizer() {
group={group}
openProductId={openProductId}
onSelectProduct={handleProductSelect}
- onApplyVariation={applyTextureWith}
/>
))}
@@ -340,7 +391,6 @@ export default function RoomVisualizer() {
product={product}
isSelected={openProductId === product.id}
onToggle={() => handleProductSelect(product.id)}
- onApplyVariation={applyTextureWith}
/>
))}
diff --git a/frontend/src/features/roomVisualizer/roomVisualizerHooks.ts b/frontend/src/features/roomVisualizer/roomVisualizerHooks.ts
index 9414561f31d8fb564a642516e59ffbbe5a288629..4990b710c535d7281d165db15c4d191df9a1c116 100644
--- a/frontend/src/features/roomVisualizer/roomVisualizerHooks.ts
+++ b/frontend/src/features/roomVisualizer/roomVisualizerHooks.ts
@@ -8,23 +8,21 @@ export function useRoomVisualizer(products: Product[]) {
const openProductId = useAppStore((store) => store.openProductId);
const setOpenProductId = useAppStore((store) => store.setOpenProductId);
- const [activeType, setActiveType] = useState("suelos");
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const filteredProducts = useMemo(() => {
const normalizedQuery = searchQuery.trim().toLowerCase();
return products.filter((product) => {
- const matchesType = product.tipo == null || product.tipo === activeType;
- const matchesQuery =
+ return (
normalizedQuery.length === 0 ||
product.brand.toLowerCase().includes(normalizedQuery) ||
product.name.toLowerCase().includes(normalizedQuery) ||
product.ref.toLowerCase().includes(normalizedQuery) ||
- product.size.toLowerCase().includes(normalizedQuery);
- return matchesType && matchesQuery;
+ product.size.toLowerCase().includes(normalizedQuery)
+ );
});
- }, [products, activeType, searchQuery]);
+ }, [products, searchQuery]);
const handleSelectProduct = useCallback(
(id: string | number | null) => {
@@ -61,8 +59,6 @@ export function useRoomVisualizer(products: Product[]) {
openProductId,
handleSelectProduct,
selectedProduct,
- activeType,
- setActiveType,
isSearchOpen,
setIsSearchOpen,
searchQuery,
diff --git a/frontend/src/features/roomVisualizer/useCatalogProducts.ts b/frontend/src/features/roomVisualizer/useCatalogProducts.ts
index e8571fdfd96f8ebf75fda601df0e22037cfb2728..b54ee65ae9685a728b8fd5a9ee3d3e064d8857b4 100644
--- a/frontend/src/features/roomVisualizer/useCatalogProducts.ts
+++ b/frontend/src/features/roomVisualizer/useCatalogProducts.ts
@@ -23,6 +23,8 @@ interface CatalogCategory {
id: string;
nombre: string;
descripcion: string;
+ especificaciones?: string[];
+ url_detalle?: string;
productos: CatalogProduct[];
}
@@ -38,7 +40,9 @@ function mapToProduct(item: CatalogProduct, category: CatalogCategory): Product
ref: item.textura,
size: item.dimensiones.length > 0 ? item.dimensiones.join(" / ") : "—",
image: `${API_BASE}${item.url_preview}`,
- tipo: CATEGORY_TYPE[category.id], // undefined si no está en el mapa → aparece en ambas tabs
+ description: category.especificaciones,
+ detailUrl: category.url_detalle,
+ tipo: CATEGORY_TYPE[category.id],
categoria: category.id,
};
}
diff --git a/frontend/src/features/shared/SharedView.tsx b/frontend/src/features/shared/SharedView.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1d1e9c8ca5e4f804013cf1bc0bd46069d57e990c
--- /dev/null
+++ b/frontend/src/features/shared/SharedView.tsx
@@ -0,0 +1,110 @@
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { Download } from "lucide-react";
+import { API_BASE } from "../../api/client";
+
+interface ShareData {
+ output_filename: string;
+ segment_filename: string | null;
+ created_at: string;
+}
+
+export default function SharedView() {
+ const { shareId } = useParams<{ shareId: string }>();
+ const [imageUrl, setImageUrl] = useState
(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!shareId) {
+ setError("ID de sesión inválido");
+ setLoading(false);
+ return;
+ }
+ fetch(`${API_BASE}/api/share/${shareId}`)
+ .then((res) => {
+ if (!res.ok) throw new Error("Sesión compartida no encontrada o expirada");
+ return res.json() as Promise;
+ })
+ .then((data) => {
+ setImageUrl(`${API_BASE}/seg/image/${data.output_filename}`);
+ })
+ .catch((err: Error) => setError(err.message))
+ .finally(() => setLoading(false));
+ }, [shareId]);
+
+ const handleDownload = async () => {
+ if (!imageUrl) return;
+ const res = await fetch(imageUrl);
+ const blob = await res.blob();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `hyper-reality-${shareId}.jpg`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ if (loading) {
+ return (
+
+ Cargando diseño compartido...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
{error}
+
+ Este enlace puede haber expirado. Los diseños compartidos se mantienen mientras el servidor esté activo.
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ Hyper Reality Visualizer
+
+
+
+
+ {/* Image */}
+
+ {imageUrl && (
+

+ )}
+
+
+ {/* Footer */}
+
+
+ );
+}
diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts
index ccc7ca3d169f3637e1246bf028466724e4bf910d..99b19c4448dd94477c1b3aa370319b5e714627ca 100644
--- a/frontend/src/store/useAppStore.ts
+++ b/frontend/src/store/useAppStore.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import { persist, createJSONStorage } from "zustand/middleware";
type ViewMode = "grid" | "list";
@@ -10,40 +11,60 @@ type AppStore = {
segmentFilename: string | null;
maskCount: number | null;
segmentProgress: number;
+ accumulatedFilename: string | null;
setPreviewImage: (previewImage: string | null) => void;
setUploadMessage: (uploadMessage: string | null) => void;
setOpenProductId: (openProductId: string | number | null) => void;
setViewMode: (viewMode: ViewMode) => void;
setSegmentResult: (filename: string, maskCount: number) => void;
setSegmentProgress: (progress: number) => void;
+ setAccumulatedFilename: (filename: string | null) => void;
reset: () => void;
};
-const useAppStore = create((set) => ({
- previewImage: null,
- uploadMessage: null,
- openProductId: null,
- viewMode: "grid",
- segmentFilename: null,
- maskCount: null,
- segmentProgress: 0,
- setPreviewImage: (previewImage) => set({ previewImage }),
- setUploadMessage: (uploadMessage) => set({ uploadMessage }),
- setOpenProductId: (openProductId) => set({ openProductId }),
- setViewMode: (viewMode) => set({ viewMode }),
- setSegmentResult: (segmentFilename, maskCount) =>
- set({ segmentFilename, maskCount }),
- setSegmentProgress: (segmentProgress) => set({ segmentProgress }),
- reset: () =>
- set({
+const useAppStore = create()(
+ persist(
+ (set) => ({
previewImage: null,
uploadMessage: null,
openProductId: null,
- viewMode: "grid",
+ viewMode: "list",
segmentFilename: null,
maskCount: null,
segmentProgress: 0,
+ accumulatedFilename: null,
+ setPreviewImage: (previewImage) => set({ previewImage }),
+ setUploadMessage: (uploadMessage) => set({ uploadMessage }),
+ setOpenProductId: (openProductId) => set({ openProductId }),
+ setViewMode: (viewMode) => set({ viewMode }),
+ setSegmentResult: (segmentFilename, maskCount) =>
+ set({ segmentFilename, maskCount }),
+ setSegmentProgress: (segmentProgress) => set({ segmentProgress }),
+ setAccumulatedFilename: (accumulatedFilename) => set({ accumulatedFilename }),
+ reset: () =>
+ set({
+ previewImage: null,
+ uploadMessage: null,
+ openProductId: null,
+ viewMode: "list",
+ segmentFilename: null,
+ maskCount: null,
+ segmentProgress: 0,
+ accumulatedFilename: null,
+ }),
}),
-}));
+ {
+ name: "hr-session",
+ storage: createJSONStorage(() => sessionStorage),
+ // Only persist the fields needed to restore the editing session
+ partialize: (state) => ({
+ previewImage: state.previewImage,
+ segmentFilename: state.segmentFilename,
+ maskCount: state.maskCount,
+ accumulatedFilename: state.accumulatedFilename,
+ }),
+ },
+ ),
+);
export default useAppStore;
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 9a03bcd30e08b313d2bff4675a195ea986bc0567..f9dc218a6c6a4469f90cb8da79ddcdc8e575f4a2 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -17,6 +17,8 @@ export interface Product {
ref: string;
size: string;
image: string;
+ description?: string[];
+ detailUrl?: string;
tipo?: "suelos" | "paredes";
categoria?: string;
}
diff --git a/frontend/src/version.ts b/frontend/src/version.ts
index 4eac75ec0891243db8140f4d8c8e36b48e4a5faa..961a8807b7d2cbb6d416ef44dcedcaa9bb575158 100644
--- a/frontend/src/version.ts
+++ b/frontend/src/version.ts
@@ -1 +1 @@
-export const appVersion = "0.1.0-dev.20260430T021631";
+export const appVersion = "0.1.0-dev.20260430T173522";