style: rediseno de layout profesional en las 7 apps
Headers sticky con marca y acciones, contenedores max-w-[1280px], grids de contenido con rails laterales sticky (cotizador, mi-dia, detalle de cobranza), stepper vertical en el wizard de alta, toolbar+ tabla unificadas en cobranza, tabs como segmented control en dashboards, chat con barra de entrada fija y preguntas sugeridas, landing con navbar + hero + grid de modulos + footer.
This commit is contained in:
@@ -1,33 +1,44 @@
|
||||
import { MODULOS } from './data/modulos'
|
||||
import { Navbar } from './components/Navbar'
|
||||
import { TarjetaModulo } from './components/TarjetaModulo'
|
||||
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="border-b border-[#E5E7EB] bg-white px-6 pt-20 pb-16 sm:px-12 lg:px-24">
|
||||
<p className="text-xs font-semibold tracking-wider text-accent uppercase">
|
||||
FRASA Desarrollos <span aria-hidden="true" className="text-[#F97316]">●</span>
|
||||
</p>
|
||||
<h1 className="mt-4 text-4xl font-light text-[#111827] sm:text-5xl">
|
||||
Ecosistema comercial
|
||||
</h1>
|
||||
</header>
|
||||
<Navbar />
|
||||
|
||||
<main className="mx-auto w-full max-w-[1280px] flex-1 px-6">
|
||||
<section aria-labelledby="titulo-hero" className="py-12 sm:py-16">
|
||||
<h1
|
||||
id="titulo-hero"
|
||||
className="text-3xl font-semibold tracking-tight text-[#111827] sm:text-4xl"
|
||||
>
|
||||
Ecosistema comercial
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#64748B] sm:text-base">
|
||||
Las herramientas del equipo comercial de Frasa Desarrollos en un solo lugar:
|
||||
cotizaciones, alta de clientes, cobranza, tableros y asistente IA.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<main className="flex-1 px-6 sm:px-12 lg:px-24">
|
||||
<nav aria-label="Módulos del ecosistema">
|
||||
<ul className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{MODULOS.map((modulo) => (
|
||||
<TarjetaModulo key={modulo.id} modulo={modulo} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<ConfiguracionCuenta />
|
||||
<div className="mt-6">
|
||||
<ConfiguracionCuenta />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
||||
<p className="text-xs text-[#64748B]">Consult (AS) Technology</p>
|
||||
<footer className="mt-16 border-t border-[#E5E7EB]">
|
||||
<div className="mx-auto max-w-[1280px] px-6 py-8">
|
||||
<p className="text-xs text-[#64748B]">Consult (AS) Technology</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -95,7 +95,11 @@ export function ConfiguracionCuenta() {
|
||||
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
|
||||
|
||||
return (
|
||||
<section aria-labelledby="titulo-cuenta" className="mt-24 border-t border-[#E5E7EB] pt-16">
|
||||
<section
|
||||
id="configuracion"
|
||||
aria-labelledby="titulo-cuenta"
|
||||
className="scroll-mt-20 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm sm:p-8"
|
||||
>
|
||||
<h2 id="titulo-cuenta" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Configuración de cuenta
|
||||
</h2>
|
||||
@@ -103,7 +107,7 @@ export function ConfiguracionCuenta() {
|
||||
{!conSesion ? (
|
||||
<form
|
||||
onSubmit={(e) => void entrar(e)}
|
||||
className="mt-8 max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm"
|
||||
className="mt-6 max-w-sm"
|
||||
>
|
||||
<p className="text-sm leading-relaxed text-[#64748B]">
|
||||
Inicia sesión para vincular tu usuario de Monday.
|
||||
@@ -150,7 +154,7 @@ export function ConfiguracionCuenta() {
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mt-8 max-w-md rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
<div className="mt-6 max-w-md">
|
||||
{cargandoPerfil && <p className="text-sm text-[#64748B]">Cargando tu perfil…</p>}
|
||||
|
||||
{perfil && (
|
||||
|
||||
46
apps/landing/src/components/Navbar.tsx
Normal file
46
apps/landing/src/components/Navbar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getSesion } from '../services/auth'
|
||||
|
||||
/** Barra superior: marca a la izquierda, estado de sesión y acceso a configuración a la derecha */
|
||||
export function Navbar() {
|
||||
const [conSesion, setConSesion] = useState(() => getSesion() !== null)
|
||||
|
||||
useEffect(() => {
|
||||
const actualizar = () => setConSesion(getSesion() !== null)
|
||||
window.addEventListener('frasa:sesion', actualizar)
|
||||
window.addEventListener('storage', actualizar)
|
||||
return () => {
|
||||
window.removeEventListener('frasa:sesion', actualizar)
|
||||
window.removeEventListener('storage', actualizar)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur">
|
||||
<div className="mx-auto flex h-14 max-w-[1280px] items-center justify-between px-6">
|
||||
<span className="text-base font-semibold tracking-tight text-[#111827]">
|
||||
FRASA<span className="text-accent">.</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span
|
||||
className={`hidden items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium sm:inline-flex ${
|
||||
conSesion ? 'bg-green-50 text-green-700' : 'bg-[#F1F5F9] text-[#64748B]'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`h-1.5 w-1.5 rounded-full ${conSesion ? 'bg-green-500' : 'bg-[#94A3B8]'}`}
|
||||
/>
|
||||
{conSesion ? 'Sesión activa' : 'Sin sesión'}
|
||||
</span>
|
||||
<a
|
||||
href="#configuracion"
|
||||
className="text-sm font-medium text-[#64748B] transition-colors hover:text-accent"
|
||||
>
|
||||
Configuración
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -4,17 +4,34 @@ interface Props {
|
||||
modulo: Modulo
|
||||
}
|
||||
|
||||
// Iniciales del "icono" de cada tarjeta; fallback: primeras 2 letras del nombre
|
||||
const INICIALES: Record<string, string> = {
|
||||
cotizador: 'CO',
|
||||
'alta-clientes': 'AC',
|
||||
cobranza: 'CB',
|
||||
'agente-ia': 'IA',
|
||||
tableros: 'TB',
|
||||
'mi-dia': 'MD',
|
||||
}
|
||||
|
||||
/** Tarjeta de acceso a un módulo del ecosistema */
|
||||
export function TarjetaModulo({ modulo }: Props) {
|
||||
const iniciales = INICIALES[modulo.id] ?? modulo.nombre.slice(0, 2).toUpperCase()
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={modulo.url}
|
||||
className="group flex h-full flex-col rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
|
||||
className="group flex h-full flex-col rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
|
||||
>
|
||||
<h2 className="text-2xl font-light text-[#111827]">{modulo.nombre}</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-[#64748B]">{modulo.descripcion}</p>
|
||||
<span className="mt-auto pt-8 text-sm font-medium text-accent transition-colors duration-100 ease-out group-hover:text-accent-hover">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50 text-sm font-semibold text-accent"
|
||||
>
|
||||
{iniciales}
|
||||
</span>
|
||||
<h2 className="mt-4 text-base font-semibold text-[#111827]">{modulo.nombre}</h2>
|
||||
<p className="mt-1 text-sm leading-relaxed text-[#64748B]">{modulo.descripcion}</p>
|
||||
<span className="mt-auto pt-6 text-sm font-medium text-accent transition-colors duration-100 ease-out group-hover:text-accent-hover">
|
||||
Entrar <span aria-hidden="true">→</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -40,6 +40,12 @@ export function getToken(): string | null {
|
||||
|
||||
export function logout(): void {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
notificarCambioSesion()
|
||||
}
|
||||
|
||||
/** Avisa a la UI (p. ej. el navbar) que cambió el estado de sesión */
|
||||
function notificarCambioSesion(): void {
|
||||
if (typeof window !== 'undefined') window.dispatchEvent(new Event('frasa:sesion'))
|
||||
}
|
||||
|
||||
/** POST /auth/login; guarda token + usuario en localStorage */
|
||||
@@ -57,6 +63,7 @@ export async function login(username: string, password: string): Promise<Usuario
|
||||
if (!res.ok) throw new ErrorLogin()
|
||||
const data = (await res.json()) as Sesion
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token: data.token, usuario: data.usuario }))
|
||||
notificarCambioSesion()
|
||||
return data.usuario
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user