08 🚀 | Construyendo el Formulario de Ajustes: Inputs con Iconos y Preview de Imagen | Laravel 12 🛠️

Duración: 23 min
Módulo: Módulo Ajustes del sistema Lección 3 de 6

Descripción

Lección 08 🚀 | Construyendo el Formulario de Ajustes: Inputs con Iconos y Preview de Imagen | Laravel 12 🛠️

¡Damos vida al corazón del sistema! En esta lección, convertimos nuestro diseño estático en un Formulario de Configuración Global completamente funcional. No se trata solo de capturar datos, sino de ofrecer una experiencia de usuario (UX) excepcional mediante validaciones, iconos dinámicos y previsualizaciones en tiempo real.

Construiremos el panel donde se definen las reglas de negocio: desde el nombre de la empresa hasta las tasas de interés y mora, integrando componentes de Flux y lógica personalizada con JavaScript. 💻✨

📑 Lo que aprenderás en esta Sesión:

  • Layout Inteligente: Implementación de un Grid responsivo (1 columna en móvil, 2 en escritorio) con Tailwind CSS. 📱💻
  • Componentes Flux Avanzados: Uso de flux:input con iconos, selectores de divisa y manejo de errores visuales. 🛠️
  • Preview de Imagen con JS: Lógica de FileReader para previsualizar el logo institucional antes de subirlo al servidor. 🖼️
  • Validación y Seguridad: Configuración de la ruta POST, el método store y el uso obligatorio de @csrf. 🛡️
  • Feedback en Tiempo Real: Cómo mostrar mensajes de error específicos debajo de cada input usando @error. ⚠️

🛠️ Detalles del Módulo de Ajustes:

  • Identidad: Nombre de la empresa, descripción y carga de logotipo.
  • Contacto: Dirección física, teléfono y email corporativo de contacto.
  • Finanzas: Selector de Divisa y configuración de porcentajes para interés y mora.
  • Modo Oscuro: Verificación de la estética del formulario en el modo dark de Flux.

📌 Capítulos de la Lección:

  • 0:00 – Introducción: Objetivos del formulario de ajustes.
  • 1:50 – Explorando componentes de Input en la librería Flux.
  • 4:10 – Creando el Grid responsivo y campos de texto con iconos.
  • 9:40 – Configurando la Ruta POST y el método Store en el controlador.
  • 12:15 – Implementando validaciones básicas y mensajes de error.
  • 16:30Masterclass JS: Lógica de previsualización para el logo.
  • 20:00 – Ajustes finales y prueba en Modo Oscuro.

💡 Tip de Oro:

"El uso de iconos dentro de los inputs no es solo estética; ayuda al usuario a identificar visualmente el propósito de cada campo de manera instantánea, mejorando la velocidad de llenado del formulario."

📢 ¡Únete a la conversación!

¿Qué te parece la integración de JavaScript para el preview del logo? ¿Prefieres este método o usarías una librería externa? ¡Te leo en los comentarios! 👇

#Laravel12 #TailwindCSS #JavaScript #WebDevelopment #Fintech #UIUX #Programacion #FluxUI

Contenido

Código fuente del video

<x-layouts.app title="Ajustes del sistema">

    <div class="relative mb-6 w-full">

        <flux:heading size="xl" level="1">Ajustes del sistema</flux:heading>

        <br>

        <flux:separator variant="subtle" />

    </div>



 

    {{-- Card --}}

    <div

        class="bg-white dark:bg-neutral-800 border-t border-gray-200 dark:border-gray-700 rounded-lg shadow-lg transition-all duration-300 hover:shadow-xl">


 

        <form action="{{ url('/admin/ajustes') }}" method="POST">

            @csrf

            {{-- Body --}}

            <div class="p-6">


 

                <!-- Formulario en grid responsivo: 1 columna en móvil, 2 en md+ -->

                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">


 

                    <div class="mb-4">

                        <flux:label>Nombre de la Empresa <span class="text-red-500" title="Campo obligatorio">

                                (*)</span></flux:label>

                        <flux:input name="nombre" icon="building-office" placeholder="Nombre Comercial" required />

                        <flux:error name="nombre" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Descripción</flux:label>

                        <flux:input name="descripcion" icon="document-text"

                            placeholder="Breve reseña de la empresa..." />

                        <flux:error name="descripcion" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Dirección <span class="text-red-500" title="Campo obligatorio">

                                (*)</span></flux:label>

                        <flux:input name="direccion" icon="map-pin" placeholder="Calle, Ciudad, País" required />

                        <flux:error name="direccion" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Teléfono <span class="text-red-500" title="Campo obligatorio">

                                (*)</span></flux:label>

                        <flux:input name="telefono" icon="phone" placeholder="+00 000 000" required />

                        <flux:error name="telefono" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Email de Contacto <span class="text-red-500" title="Campo obligatorio">

                                (*)</span></flux:label>

                        <flux:input name="email" type="email" icon="envelope" placeholder="Email de contacto"

                            required />

                        <flux:error name="email" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Divisa <span class="text-red-500" title="Campo obligatorio">

                                (*)</span></flux:label>

                        <flux:select placeholder="Selecciona una divisa..." name="divisa" required>

                            @foreach ($divisas as $divisa)

                                <flux:select.option value="{{ $divisa['symbol'] }}">{{ $divisa['name'] }}

                                </flux:select.option>

                            @endforeach

                        </flux:select>

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Tasa de Interés Mensual (%)</flux:label>

                        <flux:input name="interes" type="number" step="0.01" icon="receipt-percent"

                            placeholder="10.00" />

                        <flux:error name="interes" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Tasa de Mora (%)</flux:label>

                        <flux:input name="mora" type="number" step="0.01" icon="clock" placeholder="2.00" />

                        <flux:error name="mora" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Sitio Web</flux:label>

                        <flux:input name="web" icon="globe-alt" placeholder="www.empresa.com" />

                        <flux:error name="web" />

                    </div>


 

                    <div class="mb-4">

                        <flux:label>Logo Institucional</flux:label>


 

                        <div class="flex items-center gap-6">

                            <div class="relative group">

                                <div

                                    class="h-20 w-20 rounded-2xl border-2 border-slate-100 overflow-hidden bg-slate-50 flex items-center justify-center shadow-inner">

                                    <img id="image-preview" src="#" alt="Preview"

                                        class="hidden h-full w-full object-cover">

                                    <flux:icon id="placeholder-icon" name="photo" class="text-slate-300 h-8 w-8" />

                                </div>

                            </div>


 

                            <div class="flex flex-col gap-2">

                                <div class="flex items-center gap-3">

                                    <input type="file" name="logo" id="logo-input" class="hidden"

                                        accept="image/*">


 

                                    <label for="logo-input"

                                        class="cursor-pointer flex items-center gap-2 px-6 py-3 bg-white border-2 border-slate-200 rounded-2xl text-slate-600 font-bold hover:border-indigo-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all shadow-sm">

                                        <flux:icon name="cloud-arrow-up" variant="micro" />

                                        <span>Seleccionar Logo</span>

                                    </label>

                                </div>


 

                                <span id="file-chosen" class="text-sm text-slate-400 italic ml-1">Ningún archivo

                                    seleccionado</span>

                            </div>

                        </div>


 

                        <flux:error name="logo" />

                    </div>







 

                </div>


 

            </div>


 

            {{-- Footer --}}

            <div

                class="bg-gray-50 dark:bg-neutral-700 border-t border-gray-200 dark:border-gray-700 rounded-b-lg p-6 text-left">

                <div class="flex space-x-3">

                    <a href="{{ url('/login') }}"

                        class="px-5  text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none

                        focus:ring-2 focus:ring-gray-200 focus:ring-offset-1 transition-all duration-200 inline-flex items-center">

                        <i class="fas fa-times mr-2"></i>

                        Cancelar

                    </a>

                    <flux:button variant="primary" type="submit" class="px-5 cursor-pointer" color="blue">

                        <i class="fas fa-save mr-2"></i> Guardar

                    </flux:button>


 

                </div>

            </div>


 

        </form>


 

    </div>

    {{-- Card --}}



 

    <script>

        (function() {

            const actualBtn = document.getElementById('logo-input');

            if (!actualBtn) return;

            // prevent re-initialization when Livewire swaps DOM

            if (actualBtn.dataset.logoInitialized) return;

            actualBtn.dataset.logoInitialized = '1';


 

            const fileChosen = document.getElementById('file-chosen');

            const preview = document.getElementById('image-preview');

            const placeholderIcon = document.getElementById('placeholder-icon');


 

            actualBtn.addEventListener('change', function() {

                const file = this.files[0];


 

                if (file) {

                    // Actualizar texto del nombre

                    fileChosen.textContent = file.name;

                    fileChosen.classList.remove('text-slate-400');

                    fileChosen.classList.add('text-indigo-600', 'font-medium');


 

                    // Lógica de Previsualización

                    const reader = new FileReader();

                    reader.onload = function(e) {

                        preview.src = e.target.result;

                        preview.classList.remove('hidden');

                        placeholderIcon.classList.add('hidden');

                    }

                    reader.readAsDataURL(file);

                }

            });

        })();

    </script>

</x-layouts.app>