From 7efab390da6e70c6ea69af313e1f2792f4a8b5f0 Mon Sep 17 00:00:00 2001 From: markosbenitez Date: Mon, 29 Jun 2026 16:56:27 -0300 Subject: [PATCH] =?UTF-8?q?fix(admin):=20layout=203=20columnas=20en=20invi?= =?UTF-8?q?te=20form,=20banner=20de=20confirmaci=C3=B3n=20visible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Email/Organización/botón en una fila en desktop, apilado en mobile. El mensaje de éxito/error ahora es un banner (no texto inline) visible al menos 4s o hasta que el usuario vuelva a tocar el formulario. Co-Authored-By: Claude Sonnet 4.6 --- app/admin/usuarios/invite-form.tsx | 78 +++++++++++++++++++----------- app/globals.css | 27 +++++++++++ 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/app/admin/usuarios/invite-form.tsx b/app/admin/usuarios/invite-form.tsx index c8a01c6..89b5732 100644 --- a/app/admin/usuarios/invite-form.tsx +++ b/app/admin/usuarios/invite-form.tsx @@ -1,5 +1,5 @@ 'use client' -import { useActionState } from 'react' +import { useActionState, useEffect, useState } from 'react' import { inviteUser, type InviteUserResult } from '@/app/actions/invite-user' interface OrgOption { @@ -8,6 +8,7 @@ interface OrgOption { } const INITIAL_STATE: InviteUserResult = { success: false, error: '' } +const BANNER_VISIBLE_MS = 4000 const ERROR_MESSAGES: Record = { no_autenticado: 'Tu sesión expiró — volvé a iniciar sesión.', @@ -30,38 +31,59 @@ export function InviteForm({ organizations }: { organizations: OrgOption[] }) { async (_prev: InviteUserResult, formData: FormData) => inviteUser(formData), INITIAL_STATE ) + const [bannerVisible, setBannerVisible] = useState(false) + + // Banner visible al menos BANNER_VISIBLE_MS tras cada resultado nuevo, o + // hasta que el usuario vuelva a tocar el formulario (lo que pase primero). + useEffect(() => { + const hasMessage = state.success || (!state.success && state.error !== '') + setBannerVisible(hasMessage) + if (!hasMessage) return + const timer = setTimeout(() => setBannerVisible(false), BANNER_VISIBLE_MS) + return () => clearTimeout(timer) + }, [state]) return ( -
-
- - + setBannerVisible(false)} + > +
+
+ + +
+
+ + +
+
-
- - -
- {!state.success && state.error && ( -

{describeError(state.error)}

+ {bannerVisible && !state.success && state.error && ( +

+ {describeError(state.error)} +

)} - {state.success && ( -

Invitación enviada correctamente.

+ {bannerVisible && state.success && ( +

+ Invitación enviada correctamente. +

)} - ) } diff --git a/app/globals.css b/app/globals.css index 4092ae0..89183de 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1164,6 +1164,33 @@ body { .login-submit:hover:not(:disabled) { background: var(--color-primary-hover); } .login-submit:disabled { opacity: 0.6; cursor: not-allowed; } +/* ── Invite form (/admin/usuarios) ── */ +.invite-form { display: flex; flex-direction: column; gap: 12px; } +.invite-form-row { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 12px; + align-items: end; +} +.invite-banner { + margin: 0; + padding: 10px 14px; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 600; +} +.invite-banner-success { + background: var(--color-primary-light); + color: var(--color-primary-hover); +} +.invite-banner-error { + background: var(--color-error-bg); + color: var(--color-error); +} +@media (max-width: 768px) { + .invite-form-row { grid-template-columns: 1fr; } +} + /* ── Admin sign-out ── */ .admin-header-actions { display: flex; align-items: center; gap: 12px; } .admin-signout {