First Commit

master
dev 1 year ago
commit 3394abfcbe

24
.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web-local</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2231
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
{
"name": "web-local",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5",
"@types/node": "^24.6.0",
"svelte": "^5.39.6",
"svelte-check": "^4.3.2",
"typescript": "~5.9.3",
"vite": "^7.1.7"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"jspdf": "^3.0.3",
"tailwindcss": "^4.1.14"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

@ -0,0 +1,19 @@
<script lang="ts" xmlns="http://www.w3.org/1999/html">
import StoreTestTW from './lib/components/StoreTestTW.svelte'
import PdfTest from "./lib/components/PdfTest.svelte";
</script>
<main>
<div>
<StoreTestTW />
</div>
<div>
<PdfTest/>
</div>
</main>

@ -0,0 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,21 @@
import { type Empresa } from './data_types'
export const empresaData : Empresa = {
nombre: "Ebrosa",
logos : {
'default' : "/imgs/logo_blanco.png",
'blanco ' : "/imgs/logo_blanco.png",
'negro' : "/imgs/logo_negro.png",
'living' : "/imgs/logo_ebrosa_living.png",
'living_negro' : "/imgs/logo_ebrosa_living_negro.png",
'living_blanco': "/imgs/logo_ebrosa_living_blanco.png"
},
textos : {
'slogan' : "Slogan empresa",
}
}

@ -0,0 +1,297 @@
import { type Proyecto } from "./data_types";
export const proyecto : Proyecto = {
nombre : 'Quaddro2',
imagesEstancias : 'imgs/quaddro2/est/',
logos : {
'default' : "/imgs/quaddro2/logo_default.png"
},
textos : {
'slogan' : "Living",
'descripcion' : "Esto es la descripción"
},
materiales: {
'suelo_viggo_fresno' : {
nombre : 'Viggo Fresno',
ref : 'suelo_viggo_fresno',
imagen : '/imgs/quaddro2/mat/suelo_vigo_fresno.jpg'
},
'suelo_viggo_roble' : {
nombre : 'Viggo Roble',
ref : 'suelo_viggo_roble',
imagen : '/imgs/quaddro2/mat/suelo_vigo_roble.jpg'
},
'mueble_roble' : {
nombre : "Mueble Roble",
ref : 'mueble_roble',
imagen : '/imgs/quaddro2/mat/mueble_roble.jpg'
},
'mueble_blanco' : {
nombre : 'Mueble Blanco',
ref : 'mueble_blanco',
imagen : '/imgs/quaddro2/mat/mueble_blanco.jpg'
},
'mueble_topo' : {
nombre : 'Mueble_Blanco',
ref : 'mueble_topo',
imagen : '/imgs/quaddro2/mat/mueble_topo.jpg'
},
'ducha_viggo_roble' : {
nombre : 'Ducha Viggo Roble',
ref : 'ducha_viggo_roble',
imagen : '/imgs/quaddro2/mat/ducha_viggo_roble.jpg'
},
'ducha_viggo_fresno': {
nombre : 'Ducha Viggo Fresno',
ref : 'ducha_viggo_fresno',
imagen : '/imgs/quaddro2/mat/ducha_viggo_fresno.jpg'
},
'alicatado_agar' : {
nombre : 'Alicatado Agar Mate',
ref : 'alicatado_agar',
imagen : '/imgs/quaddro2/mat/alicatado_agar.jpg'
},
'madera_palilleria' : {
nombre : 'Palilleria',
ref : 'madera_palilleria',
imagen : '/imgs/quaddro2/mat/madera_palilleria.png'
}
},
estancias : {
// Salon
'salon' : {
nombre : 'Salón',
imagen : null,
opciones: [
{
nombre : "Suelos",
opciones: [
{
nombre : 'Suelo Viggo Fresno',
value : 'sufr',
material: 'suelo_viggo_fresno',
},
{
nombre : 'Suelo Viggo Roble',
value : 'suro',
material: 'suelo_viggo_fresno',
}
]
}
]
},
// Cocina
'cocina' : {
nombre : 'Cocina',
imagen : null,
opciones: [
{
nombre : 'Disposiciones',
opciones: [
{
nombre : 'Cocina Cerrada',
value : 'dice',
material: null,
},
{
nombre : 'Cocina Abierta',
value : 'diab',
material: null,
closed : true,
}
]
},
{
nombre : 'Suelos',
opciones: [
{
nombre : 'Suelo Viggo Fresno',
value : 'sufr',
material: 'suelo_viggo_fresno',
},
{
nombre : 'Suelo Viggo Roble',
value : 'suro',
material: 'suelo_viggo_roble'
}
]
},
{
nombre : 'Encimeras',
opciones: [
{
nombre : 'Encimera Negra',
value : 'enne',
material: null,
},
{
nombre : 'Encimera Blanca',
value : 'enbl',
material: null,
}
]
},
{
nombre : 'Revestimiento',
opciones: [
{
nombre : 'Sin Palilleria',
value : 'resi',
material: null,
},
{
nombre : 'Con Palilleria',
value : 'repa',
material: null,
}
]
},
{
nombre : 'Mobiliario',
opciones: [
{
nombre : 'Mueble Blanco',
value : 'mubl',
material: null,
},
{
nombre : 'Mueble Topo',
value : 'muto',
material: null,
}
]
}
],
},
// Baño Principal
'bano_principal' : {
nombre : 'Baño Principal',
imagen : null,
opciones: [
{
nombre : 'Disposiciónes',
opciones: [
{
nombre : 'Normal',
value : 'dino',
material: null,
},
{
nombre : 'Kit Sanitario',
value : 'diks',
material: null,
closed : true,
}
]
},
{
nombre : 'Suelos',
opciones: [
{
nombre : 'Suelo Viggo Fresno',
value : 'sufr',
material: 'suelo_viggo_fresno',
},
{
nombre : 'Suelo Viggo Roble',
material: 'suelo_viggo_Roble',
value : 'suro'
}
]
},
{
nombre : 'Alicatados Ducha',
opciones: [
{
nombre : 'Imitación Madera',
material: null,
value : 'alma',
},
{
nombre : 'Agar Mate',
material: null,
value : 'alag',
}
]
},
{
nombre : "Mobiliario Baño",
opciones: [
{
nombre : 'Mueble Blanco',
material: null,
value : 'mobl',
},
{
nombre : 'Mueble Madera',
material: null,
value : 'moma',
}
]
}
],
},
// Baño Secundario
'bano_secundario' : {
nombre : 'Baño Secundario',
imagen : null,
opciones: [
{
nombre : 'Disposiciones',
opciones: [
{
nombre: 'Con Ducha',
value : 'didu',
material: null,
},
{
nombre: 'Con Bañera',
value : 'diba',
material: null,
},
]
},
{
nombre : 'Suelos',
opciones: [
{
nombre: 'Suelo Viggo Fresno',
value : 'sufr',
material: 'suelo_viggo_fresno',
},
{
nombre: 'Suelo Viggo Roble',
value : 'suro',
material: 'suelo_viggo_roble',
},
]
},
{
nombre : "Mobiliario Baño",
opciones: [
{
nombre : 'Mueble Blanco',
material: null,
value : 'mobl',
},
{
nombre : 'Mueble Madera',
material: null,
value : 'moma',
}
]
}
]
},
},
}

@ -0,0 +1,59 @@
export interface Texto {
id : string,
text: string,
}
export interface Empresa {
nombre: string;
logos : Record<string, string>;
textos: Record<string, string>;
}
export interface Material {
nombre : string;
ref : string;
imagen?: string;
}
export interface Opcion {
nombre : string;
value : string;
material: string|null;
texto? : string;
closed? : true,
}
export interface OpcionesSet {
nombre : string;
texto? : string;
opciones : Opcion[];
}
export interface Estancia {
nombre : string;
imagen : string|null;
opciones: OpcionesSet[];
}
export interface Proyecto {
nombre : string;
imagesEstancias : string;
logos : Record<string, string>;
textos : Record<string, string>;
estancias : Record<string, Estancia>;
materiales: Record<string, Material>;
}

@ -0,0 +1,38 @@
<script lang="ts">
import { jsPDF } from 'jspdf';
function generatePdf () {
const doc = new jsPDF();
doc.text("Proyecto Quaddro2",10,10);
let img = new Image();
img.src = 'imgs/quaddro2/est/salon/sufr.jpg';
img.onload = () => {
// La imagen solo se añade DESPUÉS de que el navegador la cargó completamente
doc.addImage(img, 'JPEG', 10, 20, 180, 180); // Ajusta las dimensiones si 400x400 era muy grande
doc.save("quaddro2.pdf");
};
// **Añadir un Handler onerror**
img.onerror = () => {
console.error("Error al cargar la imagen. Revisa la ruta.");
doc.text("ERROR: Imagen no encontrada o no cargada.", 10, 30);
doc.save("quaddro2_error.pdf");
};
}
</script>
<div>
<button onclick={generatePdf} type='button'>Generar Pdf </button>
</div>

@ -0,0 +1,97 @@
<script lang="ts">
// Volvemos a importar las piezas individuales desde el archivo .svelte.ts
import { selections, updateSelection } from '../stores/selection.svelte.js';
import { proyecto } from '../../data/data_proyecto.js';
// Tus tipos están bien
interface Opcion { value: string; nombre: string; closed?: boolean; }
interface Grupo { nombre: string; opciones: Opcion[]; }
// La función getAvailableGroups usa `selections` directamente (sin prefijo $)
function getAvailableGroups(estanciaKey: string): Grupo[] {
const currentSelections = selections[estanciaKey];
if (!currentSelections) return [];
const availableGroups: Grupo[] = [];
let stop = false;
proyecto.estancias[estanciaKey].opciones.forEach((grupo: Grupo) => {
if (stop) return;
availableGroups.push(grupo);
const selectedValue = currentSelections[grupo.nombre];
const selectedOption = grupo.opciones.find((opt) => opt.value === selectedValue);
if (selectedOption?.closed) {
stop = true;
}
});
return availableGroups;
}
// 3. El estado derivado se crea con $derived, usando el estado `selections`
const imageUrls = $derived.by(() => {
const urls: Record<string, string> = {};
for (const estanciaKey in selections) {
// Obtenemos los valores de las opciones seleccionadas para esta estancia
const selectedOptions = proyecto.estancias[estanciaKey].opciones.map(grupo => selections[estanciaKey][grupo.nombre]).filter(Boolean);
const imageName = selectedOptions.length > 0 ? selectedOptions.join('_') + '.jpg' : 'default.jpg';
urls[estanciaKey] = `${proyecto.imagesEstancias}${estanciaKey}/${imageName}`;
}
return urls;
});
</script>
<div>
<h1>Verificador del Store</h1>
{#each Object.keys(proyecto.estancias) as estanciaKey}
<section>
<h2>{proyecto.estancias[estanciaKey].nombre}</h2>
{#each getAvailableGroups(estanciaKey) as grupo}
<div>
<h3>{grupo.nombre}</h3>
{#each grupo.opciones as opcion}
<label>
<input
type="radio"
name={estanciaKey + '-' + grupo.nombre}
value={opcion.value}
checked={selections[estanciaKey]?.[grupo.nombre] === opcion.value}
onchange={() => updateSelection(estanciaKey, grupo.nombre, opcion.value)}
/>
{opcion.nombre}
{#if opcion.closed}(Cerrado){/if}
</label>
{/each}
</div>
{/each}
<p>URL de la imagen: {imageUrls[estanciaKey]}</p>
<img
src={imageUrls[estanciaKey]}
alt={"Imagen de " + estanciaKey}
style="max-width: 100%;"
/>
</section>
{/each}
</div>
<style>
/* ... tus estilos no cambian ... */
section {
margin-bottom: 2em;
border: 1px solid #ccc;
padding: 1em;
}
div {
margin-bottom: 1em;
}
label {
display: block;
}
</style>

@ -0,0 +1,89 @@
<script lang="ts">
import { selections, updateSelection, setEstanciaSeleccionada, appState, getAvailableGroups, imageSrc} from '../stores/sel.svelte';
import { proyecto } from '../../data/data_proyecto';
</script>
<div class="flex min-h-screen w-full bg-gray-50">
<!-- Contenido Central -->
<main class="flex-1 flex items-center justify-center p-8">
<div class="w-full h-full flex flex-col items-center justify-center">
<h1 class="text-3xl font-bold text-gray-800 mb-4">
{proyecto.estancias[appState.estanciaSeleccionada].nombre}
</h1>
<div class="relative w-full max-w-5xl bg-white rounded-lg shadow-lg overflow-hidden">
<img
src={imageSrc()[appState.estanciaSeleccionada]}
alt={`Vista de ${proyecto.estancias[appState.estanciaSeleccionada].nombre}`}
class="w-full h-auto max-h-[70vh] object-contain"
/>
</div>
<p class="mt-4 text-sm text-gray-500 font-mono">
{imageSrc()[appState.estanciaSeleccionada]}
</p>
</div>
</main>
<!-- Sidebar Derecho -->
<aside class="w-96 flex-shrink-0 bg-white border-l border-gray-200 shadow-lg overflow-y-auto max-h-screen">
<div class="p-6 sticky top-0 bg-white z-10 border-b border-gray-100">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Configuración</h2>
<!-- Selector de Estancias -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">Estancias</h3>
<div class="space-y-2">
{#each Object.keys(proyecto.estancias) as estanciaKey}
<button
class="w-full text-left px-4 py-2 rounded-lg transition-colors {appState.estanciaSeleccionada === estanciaKey
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
onclick={() => setEstanciaSeleccionada(estanciaKey) }
>
{proyecto.estancias[estanciaKey].nombre}
</button>
{/each}
</div>
</div>
</div>
<!-- Opciones de la Estancia Seleccionada -->
<div class="p-6 pt-0">
<h3 class="text-sm font-medium text-gray-700 mb-4 mt-4">Personalización</h3>
{#each getAvailableGroups(appState.estanciaSeleccionada) as grupo}
<div class="mb-6">
<h4 class="text-sm font-semibold text-gray-800 mb-3">
{grupo.nombre}
</h4>
<div class="space-y-2">
{#each grupo.opciones as opcion}
<label class="flex items-center cursor-pointer group">
<input
type="radio"
name={`${appState.estanciaSeleccionada}-${grupo.nombre}`}
value={opcion.value}
checked={selections[appState.estanciaSeleccionada]?.[grupo.nombre] === opcion.value}
onchange={() => updateSelection(appState.estanciaSeleccionada, grupo.nombre, opcion.value)}
class="w-4 h-4 text-blue-500 border-gray-300 focus:ring-blue-500"
/>
<span class="ml-3 text-sm text-gray-700 group-hover:text-gray-900">
{opcion.nombre}
{#if opcion.closed}
<span class="ml-1 text-xs text-gray-500 italic">(Cerrado)</span>
{/if}
</span>
</label>
{/each}
</div>
</div>
{/each}
</div>
</aside>
</div>

@ -0,0 +1,134 @@
<script lang="ts">
import { selections, updateSelection } from '../stores/selection.svelte';
import { proyecto } from '../../data/data_proyecto';
interface Opcion {
value: string;
nombre: string;
closed?: boolean;
}
interface Grupo {
nombre: string;
opciones: Opcion[];
}
// Estado local para la estancia seleccionada
let estanciaSeleccionada = $state<string>(Object.keys(proyecto.estancias)[0]);
// Función para obtener grupos disponibles según la lógica 'closed'
function getAvailableGroups(estanciaKey: string): Grupo[] {
const currentSelections = selections[estanciaKey];
if (!currentSelections) return [];
const availableGroups: Grupo[] = [];
let stop = false;
proyecto.estancias[estanciaKey].opciones.forEach((grupo: Grupo) => {
if (stop) return;
availableGroups.push(grupo);
const selectedValue = currentSelections[grupo.nombre];
const selectedOption = grupo.opciones.find((opt) => opt.value === selectedValue);
if (selectedOption?.closed) {
stop = true;
}
});
return availableGroups;
}
// Estado derivado para las URLs de las imágenes
const imageUrls = $derived.by(() => {
const urls: Record<string, string> = {};
for (const estanciaKey in selections) {
const selectedOptions = proyecto.estancias[estanciaKey].opciones
.map(grupo => selections[estanciaKey][grupo.nombre])
.filter(Boolean);
const imageName = selectedOptions.length > 0 ? selectedOptions.join('_') + '.jpg' : 'default.jpg';
urls[estanciaKey] = `${proyecto.imagesEstancias}${estanciaKey}/${imageName}`;
}
return urls;
});
</script>
<div class="flex min-h-screen w-full bg-gray-50">
<!-- Contenido Central -->
<main class="flex-1 flex items-center justify-center p-8">
<div class="w-full h-full flex flex-col items-center justify-center">
<h1 class="text-3xl font-bold text-gray-800 mb-4">
{proyecto.estancias[estanciaSeleccionada].nombre}
</h1>
<div class="relative w-full max-w-5xl bg-white rounded-lg shadow-lg overflow-hidden">
<img
src={imageUrls[estanciaSeleccionada]}
alt={`Vista de ${proyecto.estancias[estanciaSeleccionada].nombre}`}
class="w-full h-auto max-h-[70vh] object-contain"
/>
</div>
<p class="mt-4 text-sm text-gray-500 font-mono">
{imageUrls[estanciaSeleccionada]}
</p>
</div>
</main>
<!-- Sidebar Derecho -->
<aside class="w-96 flex-shrink-0 bg-white border-l border-gray-200 shadow-lg overflow-y-auto max-h-screen">
<div class="p-6 sticky top-0 bg-white z-10 border-b border-gray-100">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Configuración</h2>
<!-- Selector de Estancias -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">Estancias</h3>
<div class="space-y-2">
{#each Object.keys(proyecto.estancias) as estanciaKey}
<button
class="w-full text-left px-4 py-2 rounded-lg transition-colors {estanciaSeleccionada === estanciaKey
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
onclick={() => estanciaSeleccionada = estanciaKey}
>
{proyecto.estancias[estanciaKey].nombre}
</button>
{/each}
</div>
</div>
</div>
<!-- Opciones de la Estancia Seleccionada -->
<div class="p-6 pt-0">
<h3 class="text-sm font-medium text-gray-700 mb-4 mt-4">Personalización</h3>
{#each getAvailableGroups(estanciaSeleccionada) as grupo}
<div class="mb-6">
<h4 class="text-sm font-semibold text-gray-800 mb-3">
{grupo.nombre}
</h4>
<div class="space-y-2">
{#each grupo.opciones as opcion}
<label class="flex items-center cursor-pointer group">
<input
type="radio"
name={`${estanciaSeleccionada}-${grupo.nombre}`}
value={opcion.value}
checked={selections[estanciaSeleccionada]?.[grupo.nombre] === opcion.value}
onchange={() => updateSelection(estanciaSeleccionada, grupo.nombre, opcion.value)}
class="w-4 h-4 text-blue-500 border-gray-300 focus:ring-blue-500"
/>
<span class="ml-3 text-sm text-gray-700 group-hover:text-gray-900">
{opcion.nombre}
{#if opcion.closed}
<span class="ml-1 text-xs text-gray-500 italic">(Cerrado)</span>
{/if}
</span>
</label>
{/each}
</div>
</div>
{/each}
</div>
</aside>
</div>

@ -0,0 +1,106 @@
// stores/selection.svelte.ts
import { proyecto } from '../../data/data_proyecto';
interface Opcion {
value: string;
nombre: string;
closed?: boolean;
}
interface Grupo {
nombre: string;
opciones: Opcion[];
}
// La función auxiliar para inicializar no cambia
function initializeSelections(): Record<string, Record<string, string>> {
const selections: Record<string, Record<string, string>> = {};
Object.keys(proyecto.estancias).forEach((estanciaKey) => {
const estancia = proyecto.estancias[estanciaKey];
selections[estanciaKey] = {};
estancia.opciones.forEach((grupo) => {
const defaultOption = grupo.opciones[0];
selections[estanciaKey][grupo.nombre] = defaultOption.value;
});
});
return selections;
}
// 1. El estado se define directamente con $state a nivel de módulo
export const selections = $state(initializeSelections());
// 2. La lógica de actualización es ahora una función exportada que muta `selections`
export function updateSelection(estanciaKey: string, grupoNombre: string, newValue: string) {
const estancia = proyecto.estancias[estanciaKey];
if (!estancia) return;
selections[estanciaKey][grupoNombre] = newValue;
const grupoIndex = estancia.opciones.findIndex((g) => g.nombre === grupoNombre);
if (grupoIndex === -1) return;
const grupo = estancia.opciones[grupoIndex];
const selectedOption = grupo.opciones.find((opt) => opt.value === newValue);
if (selectedOption?.closed) {
for (let i = grupoIndex + 1; i < estancia.opciones.length; i++) {
const nextGrupo = estancia.opciones[i];
delete selections[estanciaKey][nextGrupo.nombre];
}
} else {
for (let i = grupoIndex + 1; i < estancia.opciones.length; i++) {
const nextGrupo = estancia.opciones[i];
if (!selections[estanciaKey][nextGrupo.nombre]) {
selections[estanciaKey][nextGrupo.nombre] = nextGrupo.opciones[0].value;
}
}
}
}
// Función para obtener grupos disponibles según la lógica 'closed' (ahora exportada y usando el estado global)
export function getAvailableGroups(estanciaKey: string): Grupo[] {
const currentSelections = selections[estanciaKey];
if (!currentSelections) return [];
const availableGroups: Grupo[] = [];
let stop = false;
proyecto.estancias[estanciaKey].opciones.forEach((grupo: Grupo) => {
if (stop) return;
availableGroups.push(grupo);
const selectedValue = currentSelections[grupo.nombre];
const selectedOption = grupo.opciones.find((opt) => opt.value === selectedValue);
if (selectedOption?.closed) {
stop = true;
}
});
return availableGroups;
}
// Estado derivado global para las URLs de las imágenes
const imageUrls = $derived.by(() => {
const urls: Record<string, string> = {};
for (const estanciaKey in selections) {
const selectedOptions = proyecto.estancias[estanciaKey].opciones
.map(grupo => selections[estanciaKey][grupo.nombre])
.filter(Boolean);
const imageName = selectedOptions.length > 0 ? selectedOptions.join('_') + '.jpg' : 'default.jpg';
urls[estanciaKey] = `${proyecto.imagesEstancias}${estanciaKey}/${imageName}`;
}
return urls;
});
export const appState = $state({
estanciaSeleccionada : Object.keys(proyecto.estancias)[0]
});
export function imageSrc () : Record<string, string> {
return imageUrls
}
export function setEstanciaSeleccionada(value : string) {
appState.estanciaSeleccionada = value;
}

@ -0,0 +1,48 @@
// stores/selection.svelte.ts
import { proyecto } from '../../data/data_proyecto';
// La función auxiliar para inicializar no cambia
function initializeSelections(): Record<string, Record<string, string>> {
const selections: Record<string, Record<string, string>> = {};
Object.keys(proyecto.estancias).forEach((estanciaKey) => {
const estancia = proyecto.estancias[estanciaKey];
selections[estanciaKey] = {};
estancia.opciones.forEach((grupo) => {
const defaultOption = grupo.opciones[0];
selections[estanciaKey][grupo.nombre] = defaultOption.value;
});
});
return selections;
}
// 1. El estado se define directamente con $state a nivel de módulo
export const selections = $state(initializeSelections());
// 2. La lógica de actualización es ahora una función exportada que muta `selections`
export function updateSelection(estanciaKey: string, grupoNombre: string, newValue: string) {
const estancia = proyecto.estancias[estanciaKey];
if (!estancia) return;
selections[estanciaKey][grupoNombre] = newValue;
const grupoIndex = estancia.opciones.findIndex((g) => g.nombre === grupoNombre);
if (grupoIndex === -1) return;
const grupo = estancia.opciones[grupoIndex];
const selectedOption = grupo.opciones.find((opt) => opt.value === newValue);
if (selectedOption?.closed) {
for (let i = grupoIndex + 1; i < estancia.opciones.length; i++) {
const nextGrupo = estancia.opciones[i];
delete selections[estanciaKey][nextGrupo.nombre];
}
} else {
for (let i = grupoIndex + 1; i < estancia.opciones.length; i++) {
const nextGrupo = estancia.opciones[i];
if (!selections[estanciaKey][nextGrupo.nombre]) {
selections[estanciaKey][nextGrupo.nombre] = nextGrupo.opciones[0].value;
}
}
}
}

@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

@ -0,0 +1,8 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

@ -0,0 +1,21 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"types": ["svelte", "vite/client"],
"noEmit": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"moduleDetection": "force"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

@ -0,0 +1,9 @@
{
"allowArbitraryExtensions": true,
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,18 @@
import {defineConfig} from 'vite'
import tailwindcss from "@tailwindcss/vite";
import {svelte} from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
base: './',
plugins: [svelte(),tailwindcss()],
build: {
rollupOptions: {
output: {
inlineDynamicImports: true,
manualChunks: undefined
}
}
}
})
Loading…
Cancel
Save

Powered by TurnKey Linux.