fix(admin): layout 3 columnas en invite form, banner de confirmación visible
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useActionState } from 'react'
|
import { useActionState, useEffect, useState } from 'react'
|
||||||
import { inviteUser, type InviteUserResult } from '@/app/actions/invite-user'
|
import { inviteUser, type InviteUserResult } from '@/app/actions/invite-user'
|
||||||
|
|
||||||
interface OrgOption {
|
interface OrgOption {
|
||||||
@@ -8,6 +8,7 @@ interface OrgOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_STATE: InviteUserResult = { success: false, error: '' }
|
const INITIAL_STATE: InviteUserResult = { success: false, error: '' }
|
||||||
|
const BANNER_VISIBLE_MS = 4000
|
||||||
|
|
||||||
const ERROR_MESSAGES: Record<string, string> = {
|
const ERROR_MESSAGES: Record<string, string> = {
|
||||||
no_autenticado: 'Tu sesión expiró — volvé a iniciar sesión.',
|
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),
|
async (_prev: InviteUserResult, formData: FormData) => inviteUser(formData),
|
||||||
INITIAL_STATE
|
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 (
|
return (
|
||||||
<form action={formAction} className="login-form">
|
<form
|
||||||
<div className="login-field">
|
action={formAction}
|
||||||
<label htmlFor="email" className="login-label">Email</label>
|
className="invite-form"
|
||||||
<input
|
onChange={() => setBannerVisible(false)}
|
||||||
id="email"
|
>
|
||||||
name="email"
|
<div className="invite-form-row">
|
||||||
type="email"
|
<div className="login-field">
|
||||||
required
|
<label htmlFor="email" className="login-label">Email</label>
|
||||||
disabled={isPending}
|
<input
|
||||||
className="text-short-input"
|
id="email"
|
||||||
/>
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
className="text-short-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-field">
|
||||||
|
<label htmlFor="orgId" className="login-label">Organización</label>
|
||||||
|
<select id="orgId" name="orgId" required disabled={isPending} className="admin-survey-select">
|
||||||
|
<option value="">Seleccioná una organización</option>
|
||||||
|
{organizations.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>{org.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isPending} className="login-submit">
|
||||||
|
{isPending ? 'Enviando…' : 'Enviar invitación'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="login-field">
|
{bannerVisible && !state.success && state.error && (
|
||||||
<label htmlFor="orgId" className="login-label">Organización</label>
|
<p role="alert" className="invite-banner invite-banner-error">
|
||||||
<select id="orgId" name="orgId" required disabled={isPending} className="admin-survey-select">
|
{describeError(state.error)}
|
||||||
<option value="">Seleccioná una organización</option>
|
</p>
|
||||||
{organizations.map((org) => (
|
|
||||||
<option key={org.id} value={org.id}>{org.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{!state.success && state.error && (
|
|
||||||
<p role="alert" className="field-error login-error">{describeError(state.error)}</p>
|
|
||||||
)}
|
)}
|
||||||
{state.success && (
|
{bannerVisible && state.success && (
|
||||||
<p className="admin-subtitle" role="status">Invitación enviada correctamente.</p>
|
<p role="status" className="invite-banner invite-banner-success">
|
||||||
|
Invitación enviada correctamente.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<button type="submit" disabled={isPending} className="login-submit">
|
|
||||||
{isPending ? 'Enviando…' : 'Enviar invitación'}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1164,6 +1164,33 @@ body {
|
|||||||
.login-submit:hover:not(:disabled) { background: var(--color-primary-hover); }
|
.login-submit:hover:not(:disabled) { background: var(--color-primary-hover); }
|
||||||
.login-submit:disabled { opacity: 0.6; cursor: not-allowed; }
|
.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 sign-out ── */
|
||||||
.admin-header-actions { display: flex; align-items: center; gap: 12px; }
|
.admin-header-actions { display: flex; align-items: center; gap: 12px; }
|
||||||
.admin-signout {
|
.admin-signout {
|
||||||
|
|||||||
Reference in New Issue
Block a user