Improve dark mode with Zinc color palette

- Change from gray to zinc colors for a neutral cool aesthetic
- Use zinc-950 for main background, zinc-900 for cards
- Add subtle borders to cards in dark mode for better separation
- Update all components: Home, TopMenu, connector pages
- More elegant and minimalist dark mode appearance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Exteban08
2026-02-03 11:51:03 +00:00
parent 9b8d6c4e45
commit c741b697d9
9 changed files with 217 additions and 216 deletions

View File

@@ -254,7 +254,7 @@ export default function App() {
</div> </div>
{/* ✅ AQUÍ VA LA MARCA DE AGUA */} {/* ✅ AQUÍ VA LA MARCA DE AGUA */}
<main className="relative min-w-0 flex-1 overflow-auto bg-slate-50 dark:bg-gray-900"> <main className="relative min-w-0 flex-1 overflow-auto bg-slate-50 dark:bg-zinc-950">
<Watermark /> <Watermark />
<div className="relative z-10">{renderPage()}</div> <div className="relative z-10">{renderPage()}</div>
</main> </main>

View File

@@ -180,14 +180,14 @@ const TopMenu: React.FC<TopMenuProps> = ({
role="menu" role="menu"
className=" className="
absolute right-0 mt-2 w-80 absolute right-0 mt-2 w-80
rounded-2xl bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-2xl bg-white dark:bg-zinc-900 border border-slate-200 dark:border-zinc-800
shadow-xl overflow-hidden z-50 shadow-xl overflow-hidden z-50
" "
> >
{/* Header usuario */} {/* Header usuario */}
<div className="px-5 py-4 border-b border-slate-200 dark:border-gray-700"> <div className="px-5 py-4 border-b border-slate-200 dark:border-zinc-800">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-full bg-slate-100 dark:bg-gray-700 overflow-hidden flex items-center justify-center"> <div className="w-11 h-11 rounded-full bg-slate-100 dark:bg-zinc-800 overflow-hidden flex items-center justify-center">
{avatarUrl ? ( {avatarUrl ? (
<img <img
src={avatarUrl} src={avatarUrl}
@@ -195,7 +195,7 @@ const TopMenu: React.FC<TopMenuProps> = ({
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<span className="text-sm font-semibold text-slate-700 dark:text-gray-200"> <span className="text-sm font-semibold text-slate-700 dark:text-zinc-200">
{initials} {initials}
</span> </span>
)} )}
@@ -206,11 +206,11 @@ const TopMenu: React.FC<TopMenuProps> = ({
{userName} {userName}
</div> </div>
{userEmail ? ( {userEmail ? (
<div className="text-xs text-slate-500 dark:text-gray-400 truncate"> <div className="text-xs text-slate-500 dark:text-zinc-400 truncate">
{userEmail} {userEmail}
</div> </div>
) : ( ) : (
<div className="text-xs text-slate-400 dark:text-gray-500 truncate"></div> <div className="text-xs text-slate-400 dark:text-zinc-500 truncate"></div>
)} )}
</div> </div>
</div> </div>
@@ -225,7 +225,7 @@ const TopMenu: React.FC<TopMenuProps> = ({
left={<User size={16} />} left={<User size={16} />}
/> />
<div className="h-px bg-slate-200 dark:bg-gray-700 my-1" /> <div className="h-px bg-slate-200 dark:bg-zinc-800 my-1" />
<MenuItem <MenuItem
label="Cerrar sesión" label="Cerrar sesión"
@@ -266,13 +266,13 @@ function MenuItem({
className={[ className={[
"w-full flex items-center gap-3 px-5 py-3 text-sm text-left", "w-full flex items-center gap-3 px-5 py-3 text-sm text-left",
"transition-colors", "transition-colors",
disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-slate-100 dark:hover:bg-gray-700", disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-slate-100 dark:hover:bg-zinc-800",
tone === "danger" tone === "danger"
? "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" ? "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
: "text-slate-700 dark:text-gray-200", : "text-slate-700 dark:text-zinc-200",
].join(" ")} ].join(" ")}
> >
<span className="text-slate-400 dark:text-gray-500">{left}</span> <span className="text-slate-400 dark:text-zinc-500">{left}</span>
<span className="font-medium">{label}</span> <span className="font-medium">{label}</span>
</button> </button>
); );

View File

@@ -32,7 +32,7 @@ export function useNotifications(autoRefreshInterval: number = 30000): UseNotifi
const [hasMore, setHasMore] = useState<boolean>(false); const [hasMore, setHasMore] = useState<boolean>(false);
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null); const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fetchNotifications = useCallback(async (filters?: NotificationFilters) => { const fetchNotifications = useCallback(async (filters?: NotificationFilters) => {
try { try {

View File

@@ -14,10 +14,10 @@
/* Dark mode body */ /* Dark mode body */
body { body {
@apply bg-slate-50 text-gray-900; @apply bg-slate-50 text-zinc-900;
} }
.dark body, .dark body,
body:where(.dark *) { body:where(.dark *) {
@apply bg-gray-900 text-gray-100; @apply bg-zinc-950 text-zinc-100;
} }

View File

@@ -395,7 +395,7 @@ export default function Home({
<h1 className="text-3xl font-bold text-gray-800 dark:text-white"> <h1 className="text-3xl font-bold text-gray-800 dark:text-white">
Sistema de Tomas de Agua Sistema de Tomas de Agua
</h1> </h1>
<p className="text-gray-600 dark:text-gray-300 mt-2"> <p className="text-gray-600 dark:text-zinc-300 mt-2">
Monitorea, administra y controla tus operaciones en un solo lugar. Monitorea, administra y controla tus operaciones en un solo lugar.
</p> </p>
</div> </div>
@@ -412,42 +412,42 @@ export default function Home({
{/* Cards de Secciones */} {/* Cards de Secciones */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div <div
className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-blue-50 dark:hover:bg-gray-700 transition cursor-pointer" className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-blue-50 dark:hover:bg-zinc-800 transition cursor-pointer"
onClick={() => setPage("meters")} onClick={() => setPage("meters")}
> >
<Cpu size={40} className="text-blue-600" /> <Cpu size={40} className="text-blue-600" />
<span className="font-semibold text-gray-700 dark:text-gray-200">Tomas</span> <span className="font-semibold text-gray-700 dark:text-zinc-200">Tomas</span>
</div> </div>
<div <div
className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-red-50 dark:hover:bg-gray-700 transition cursor-pointer" className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-red-50 dark:hover:bg-zinc-800 transition cursor-pointer"
onClick={() => setPage("auditoria")} onClick={() => setPage("auditoria")}
> >
<Bell size={40} className="text-red-600" /> <Bell size={40} className="text-red-600" />
<span className="font-semibold text-gray-700 dark:text-gray-200">Alertas</span> <span className="font-semibold text-gray-700 dark:text-zinc-200">Alertas</span>
</div> </div>
<div className="cursor-pointer bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-yellow-50 dark:hover:bg-gray-700 transition" <div className="cursor-pointer bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-yellow-50 dark:hover:bg-zinc-800 transition"
onClick={() => setPage("projects")} onClick={() => setPage("projects")}
> >
<Settings size={40} className="text-yellow-600" /> <Settings size={40} className="text-yellow-600" />
<span className="font-semibold text-gray-700 dark:text-gray-200">Proyectos</span> <span className="font-semibold text-gray-700 dark:text-zinc-200">Proyectos</span>
</div> </div>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-green-50 dark:hover:bg-gray-700 transition"> <div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-green-50 dark:hover:bg-zinc-800 transition">
<BarChart3 size={40} className="text-green-600" /> <BarChart3 size={40} className="text-green-600" />
<span className="font-semibold text-gray-700 dark:text-gray-200">Reportes</span> <span className="font-semibold text-gray-700 dark:text-zinc-200">Reportes</span>
</div> </div>
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-4"> <div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div> <div>
<p className="text-sm text-gray-500 dark:text-gray-400">Organismos Operadores</p> <p className="text-sm text-gray-500 dark:text-zinc-400">Organismos Operadores</p>
<p className="text-xs text-gray-400 dark:text-gray-500"> <p className="text-xs text-gray-400 dark:text-zinc-500">
Seleccionado:{" "} Seleccionado:{" "}
<span className="font-semibold dark:text-gray-300"> <span className="font-semibold dark:text-zinc-300">
{selectedOrganism === "Todos" {selectedOrganism === "Todos"
? "Todos" ? "Todos"
: organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"} : organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"}
@@ -476,21 +476,21 @@ export default function Home({
/> />
{/* Panel */} {/* Panel */}
<div className="relative w-full max-w-2xl max-h-[90vh] bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-col"> <div className="relative w-full max-w-2xl max-h-[90vh] bg-white dark:bg-zinc-900 rounded-xl shadow-2xl flex flex-col">
{/* Header */} {/* Header */}
<div className="p-5 border-b dark:border-gray-700 flex items-start justify-between gap-3"> <div className="p-5 border-b dark:border-zinc-800 flex items-start justify-between gap-3">
<div> <div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white"> <h3 className="text-lg font-semibold text-gray-800 dark:text-white">
Organismos Operadores Organismos Operadores
</h3> </h3>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-zinc-400">
Selecciona un organismo para filtrar la información del dashboard Selecciona un organismo para filtrar la información del dashboard
</p> </p>
</div> </div>
<button <button
type="button" type="button"
className="rounded-lg px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300" className="rounded-lg px-3 py-2 text-sm border border-gray-300 dark:border-zinc-700 hover:bg-gray-50 dark:hover:bg-zinc-800 dark:text-zinc-300"
onClick={() => { onClick={() => {
setShowOrganisms(false); setShowOrganisms(false);
setOrganismQuery(""); setOrganismQuery("");
@@ -501,12 +501,12 @@ export default function Home({
</div> </div>
{/* Search */} {/* Search */}
<div className="p-5 border-b dark:border-gray-700"> <div className="p-5 border-b dark:border-zinc-800">
<input <input
value={organismQuery} value={organismQuery}
onChange={(e) => setOrganismQuery(e.target.value)} onChange={(e) => setOrganismQuery(e.target.value)}
placeholder="Buscar organismo…" placeholder="Buscar organismo…"
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500 dark:placeholder-gray-400" className="w-full rounded-lg border border-gray-300 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500 dark:placeholder-gray-400"
/> />
</div> </div>
@@ -523,7 +523,7 @@ export default function Home({
"rounded-xl border p-4 transition", "rounded-xl border p-4 transition",
selectedOrganism === "Todos" selectedOrganism === "Todos"
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20" ? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20"
: "border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600", : "border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700",
].join(" ")} ].join(" ")}
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@@ -531,7 +531,7 @@ export default function Home({
<p className="text-sm font-semibold text-gray-800 dark:text-white"> <p className="text-sm font-semibold text-gray-800 dark:text-white">
Todos los Organismos Todos los Organismos
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400">Ver todos los datos del sistema</p> <p className="text-xs text-gray-500 dark:text-zinc-400">Ver todos los datos del sistema</p>
</div> </div>
<span className="text-xs font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-700"> <span className="text-xs font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-700">
@@ -569,7 +569,7 @@ export default function Home({
"rounded-xl border p-4 transition", "rounded-xl border p-4 transition",
active active
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20" ? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20"
: "border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600", : "border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700",
].join(" ")} ].join(" ")}
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@@ -577,7 +577,7 @@ export default function Home({
<p className="text-sm font-semibold text-gray-800 dark:text-white"> <p className="text-sm font-semibold text-gray-800 dark:text-white">
{o.name} {o.name}
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400">{o.region}</p> <p className="text-xs text-gray-500 dark:text-zinc-400">{o.region}</p>
</div> </div>
<span <span
@@ -594,36 +594,36 @@ export default function Home({
<div className="mt-3 space-y-2 text-xs"> <div className="mt-3 space-y-2 text-xs">
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500 dark:text-gray-400">Rol</span> <span className="text-gray-500 dark:text-zinc-400">Rol</span>
<span className="font-medium text-gray-800 dark:text-gray-200"> <span className="font-medium text-gray-800 dark:text-zinc-200">
{o.contact} {o.contact}
</span> </span>
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500 dark:text-gray-400">Email</span> <span className="text-gray-500 dark:text-zinc-400">Email</span>
<span className="font-medium text-gray-800 dark:text-gray-200 truncate max-w-[200px]"> <span className="font-medium text-gray-800 dark:text-zinc-200 truncate max-w-[200px]">
{o.region} {o.region}
</span> </span>
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500 dark:text-gray-400">Proyectos</span> <span className="text-gray-500 dark:text-zinc-400">Proyectos</span>
<span className="font-medium text-gray-800 dark:text-gray-200"> <span className="font-medium text-gray-800 dark:text-zinc-200">
{o.projects} {o.projects}
</span> </span>
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500 dark:text-gray-400">Medidores</span> <span className="text-gray-500 dark:text-zinc-400">Medidores</span>
<span className="font-medium text-gray-800 dark:text-gray-200"> <span className="font-medium text-gray-800 dark:text-zinc-200">
{o.meters} {o.meters}
</span> </span>
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500 dark:text-gray-400">Último acceso</span> <span className="text-gray-500 dark:text-zinc-400">Último acceso</span>
<span className="font-medium text-gray-800 dark:text-gray-200"> <span className="font-medium text-gray-800 dark:text-zinc-200">
{o.lastSync} {o.lastSync}
</span> </span>
</div> </div>
@@ -654,14 +654,14 @@ export default function Home({
)} )}
{!loadingUsers && filteredOrganisms.length === 0 && ( {!loadingUsers && filteredOrganisms.length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400 text-center py-10"> <div className="text-sm text-gray-500 dark:text-zinc-400 text-center py-10">
No se encontraron organismos. No se encontraron organismos.
</div> </div>
)} )}
</div> </div>
{/* Footer */} {/* Footer */}
<div className="p-5 border-t dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400"> <div className="p-5 border-t dark:border-zinc-800 text-xs text-gray-500 dark:text-zinc-400">
Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''} Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''}
</div> </div>
</div> </div>
@@ -672,7 +672,7 @@ export default function Home({
</div> </div>
{/* Gráfica */} {/* Gráfica */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6"> <div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6">
<div className="flex items-center justify-between gap-4 mb-4"> <div className="flex items-center justify-between gap-4 mb-4">
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
Numero de Medidores por Proyecto Numero de Medidores por Proyecto
@@ -684,20 +684,20 @@ export default function Home({
{chartData.length === 0 && selectedOrganism !== "Todos" ? ( {chartData.length === 0 && selectedOrganism !== "Todos" ? (
<div className="h-60 flex flex-col items-center justify-center"> <div className="h-60 flex flex-col items-center justify-center">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2"> <p className="text-sm text-gray-500 dark:text-zinc-400 mb-2">
{selectedUserProjectName {selectedUserProjectName
? "Este organismo no tiene medidores registrados" ? "Este organismo no tiene medidores registrados"
: "Este organismo no tiene un proyecto asignado"} : "Este organismo no tiene un proyecto asignado"}
</p> </p>
{selectedUserProjectName && ( {selectedUserProjectName && (
<p className="text-xs text-gray-400 dark:text-gray-500"> <p className="text-xs text-gray-400 dark:text-zinc-500">
Proyecto asignado: <span className="font-semibold dark:text-gray-300">{selectedUserProjectName}</span> Proyecto asignado: <span className="font-semibold dark:text-zinc-300">{selectedUserProjectName}</span>
</p> </p>
)} )}
</div> </div>
) : chartData.length === 0 ? ( ) : chartData.length === 0 ? (
<div className="h-60 flex items-center justify-center"> <div className="h-60 flex items-center justify-center">
<p className="text-sm text-gray-500 dark:text-gray-400">No hay datos disponibles</p> <p className="text-sm text-gray-500 dark:text-zinc-400">No hay datos disponibles</p>
</div> </div>
) : ( ) : (
<> <>
@@ -718,14 +718,14 @@ export default function Home({
</div> </div>
{selectedOrganism !== "Todos" && selectedUserProjectName && ( {selectedOrganism !== "Todos" && selectedUserProjectName && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> <div className="mt-4 pt-4 border-t border-gray-200 dark:border-zinc-800">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<div> <div>
<span className="text-gray-500 dark:text-gray-400">Proyecto del organismo:</span> <span className="text-gray-500 dark:text-zinc-400">Proyecto del organismo:</span>
<span className="ml-2 font-semibold text-gray-800 dark:text-white">{selectedUserProjectName}</span> <span className="ml-2 font-semibold text-gray-800 dark:text-white">{selectedUserProjectName}</span>
</div> </div>
<div> <div>
<span className="text-gray-500 dark:text-gray-400">Total de medidores:</span> <span className="text-gray-500 dark:text-zinc-400">Total de medidores:</span>
<span className="ml-2 font-semibold text-blue-600">{filteredMeters.length}</span> <span className="ml-2 font-semibold text-blue-600">{filteredMeters.length}</span>
</div> </div>
</div> </div>
@@ -736,7 +736,7 @@ export default function Home({
</div> </div>
{!isOperator && ( {!isOperator && (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6"> <div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6">
<h2 className="text-lg font-semibold mb-4 dark:text-white">Historial Reciente de Auditoria</h2> <h2 className="text-lg font-semibold mb-4 dark:text-white">Historial Reciente de Auditoria</h2>
{loadingAuditLogs ? ( {loadingAuditLogs ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
@@ -747,12 +747,12 @@ export default function Home({
No hay registros de auditoría disponibles No hay registros de auditoría disponibles
</p> </p>
) : ( ) : (
<ul className="divide-y divide-gray-200 dark:divide-gray-700 max-h-60 overflow-y-auto"> <ul className="divide-y divide-gray-200 dark:divide-zinc-800 max-h-60 overflow-y-auto">
{history.map((h, i) => ( {history.map((h, i) => (
<li key={i} className="py-2 flex items-start gap-3"> <li key={i} className="py-2 flex items-start gap-3">
<span className="text-gray-400 mt-1"></span> <span className="text-gray-400 mt-1"></span>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-700 dark:text-gray-300"> <p className="text-sm text-gray-700 dark:text-zinc-300">
<span className="font-semibold">{h.user}</span> {h.action}{" "} <span className="font-semibold">{h.user}</span> {h.action}{" "}
<span className="font-medium">{h.target}</span> <span className="font-medium">{h.target}</span>
</p> </p>
@@ -766,7 +766,7 @@ export default function Home({
)} )}
{!isOperator && ( {!isOperator && (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6"> <div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-6">
<h2 className="text-lg font-semibold mb-4 dark:text-white">Ultimas Alertas</h2> <h2 className="text-lg font-semibold mb-4 dark:text-white">Ultimas Alertas</h2>
{loadingNotifications ? ( {loadingNotifications ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
@@ -777,11 +777,11 @@ export default function Home({
No hay alertas disponibles No hay alertas disponibles
</p> </p>
) : ( ) : (
<ul className="divide-y divide-gray-200 dark:divide-gray-700"> <ul className="divide-y divide-gray-200 dark:divide-zinc-800">
{alerts.map((a, i) => ( {alerts.map((a, i) => (
<li key={i} className="py-2 flex justify-between items-start"> <li key={i} className="py-2 flex justify-between items-start">
<div className="flex-1"> <div className="flex-1">
<span className="text-sm text-gray-700 dark:text-gray-300"> <span className="text-sm text-gray-700 dark:text-zinc-300">
<span className="font-semibold">{a.company}</span> - {a.type} <span className="font-semibold">{a.company}</span> - {a.type}
</span> </span>
</div> </div>

View File

@@ -32,7 +32,6 @@ export default function LoginPage({ onSuccess }: LoginPageProps) {
setLoading(true); setLoading(true);
try { try {
await login({ email: form.email, password: form.password }); await login({ email: form.email, password: form.password });
// Tokens are stored by the auth module
onSuccess(); onSuccess();
} catch (err) { } catch (err) {
setServerError(err instanceof Error ? err.message : "Error de autenticación"); setServerError(err instanceof Error ? err.message : "Error de autenticación");
@@ -42,153 +41,155 @@ export default function LoginPage({ onSuccess }: LoginPageProps) {
} }
return ( return (
<div className="h-screen w-screen font-sans bg-slate-50"> <div className="fixed inset-0 w-screen h-screen font-sans">
<div className="relative h-full w-full overflow-hidden bg-white"> {/* Imagen de fondo - agua */}
<div <div
className="absolute left-0 top-0 h-[3px] w-full opacity-90" className="absolute inset-0 w-full h-full bg-cover bg-center"
style={{ style={{
background: backgroundImage: `url('https://images.unsplash.com/photo-1500375592092-40eb2168fd21?q=80&w=2088&auto=format&fit=crop')`,
"linear-gradient(90deg, transparent, rgba(86,107,184,0.9), rgba(76,95,158,0.9), transparent)", }}
}} />
/>
<div className="grid h-full grid-cols-1 md:grid-cols-2"> {/* Overlay oscuro */}
{/* IZQUIERDA */} <div className="absolute inset-0 bg-black/50" />
<section className="relative overflow-hidden">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, #2a355d 10%, #4c5f9e 55%, #566bb8 100%)",
}}
/>
<div {/* Layout dividido */}
className="absolute inset-0" <div className="relative z-10 w-full h-full flex">
style={{ {/* Lado izquierdo - Branding */}
clipPath: "polygon(0 0, 80% 0, 55% 100%, 0 100%)", <div className="hidden lg:flex lg:w-1/2 h-full flex-col justify-between p-12">
background: <div className="flex items-center gap-4">
"linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.02))", <div className="h-24 w-24 rounded-2xl bg-white/90 backdrop-blur-sm shadow-xl flex items-center justify-center">
}} <img
/> src={grhWatermark}
alt="GRH"
<div className="relative h-full flex items-center px-10 md:px-12"> className="h-18 w-18 object-contain"
<div className="max-w-sm text-white"> />
<h2 className="text-4xl font-semibold tracking-tight">
¡Bienvenido!
</h2>
<p className="mt-3 text-sm leading-relaxed text-white/90">
Ingresa con tus credenciales para acceder al panel GRH.
</p>
</div>
</div> </div>
</section> <span className="text-white text-4xl font-bold tracking-tight drop-shadow-lg">
GRH
</span>
</div>
{/* DERECHA */} <div className="max-w-xl">
<section className="bg-white flex items-center justify-center"> <h1 className="text-6xl font-bold text-white leading-tight drop-shadow-lg">
<div className="w-full max-w-lg px-6"> Gestión de
<div className="rounded-3xl border border-slate-200 bg-white/90 p-10 md:p-12 shadow-lg"> <br />
<div className="flex items-center gap-4"> Recursos Hídricos
<img </h1>
src={grhWatermark} <p className="mt-6 text-xl text-white/90 leading-relaxed drop-shadow">
alt="GRH" Llevando agua potable a comunidades que más lo necesitan.
className="h-20 w-20 object-contain rounded-lg" Monitoreo inteligente de infraestructura hídrica.
/> </p>
<div className="leading-tight"> </div>
<h1 className="text-2xl font-semibold text-slate-900">
Iniciar sesión <p className="text-white/60 text-sm">
</h1> © 2026 GRH. Todos los derechos reservados.
<p className="text-xs text-slate-500"> </p>
Gestión de Recursos Hídricos </div>
</p>
</div> {/* Lado derecho - Formulario */}
<div className="w-full lg:w-1/2 h-full flex items-center justify-center p-6">
<div className="w-full max-w-md bg-white rounded-2xl shadow-2xl p-8 md:p-10">
{/* Logo móvil */}
<div className="flex lg:hidden items-center gap-3 mb-8">
<img
src={grhWatermark}
alt="GRH"
className="h-10 w-10 object-contain"
/>
<span className="text-slate-800 text-lg font-semibold">GRH</span>
</div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-slate-900">
Iniciar sesión
</h2>
<p className="mt-2 text-slate-500 text-sm">
Ingresa tus credenciales para acceder al sistema
</p>
</div>
<form onSubmit={onSubmit} className="space-y-5">
{serverError && (
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
{serverError}
</div> </div>
)}
<form onSubmit={onSubmit} className="mt-8 space-y-6"> {/* Email */}
{serverError && ( <div>
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700"> <label className="block text-sm font-medium text-slate-700 mb-1.5">
{serverError} Correo electrónico
</div> </label>
)} <div className="relative">
<input
{/* Email */} type="email"
<div> value={form.email}
<label className="block text-sm font-medium text-slate-700"> onChange={(e) =>
Correo electrónico setForm((s) => ({ ...s, email: e.target.value }))
</label> }
<div className="relative mt-2"> placeholder="usuario@ejemplo.com"
<input className="w-full rounded-lg border border-slate-300 bg-white px-4 py-3 pr-11 text-slate-900 placeholder-slate-400 outline-none transition-all focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
type="email" />
value={form.email} <Mail
onChange={(e) => className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400"
setForm((s) => ({ ...s, email: e.target.value })) size={18}
} />
className="w-full border-b border-slate-300 py-2 pr-10 outline-none focus:border-slate-600" </div>
/> {errors.email && (
<Mail <p className="mt-1.5 text-xs text-red-600">{errors.email}</p>
className="absolute right-1 top-1/2 -translate-y-1/2 text-slate-500" )}
size={18}
/>
</div>
{errors.email && (
<p className="mt-1 text-xs text-red-600">
{errors.email}
</p>
)}
</div>
{/* Contraseña */}
<div>
<label className="block text-sm font-medium text-slate-700">
Contraseña
</label>
<div className="relative mt-2">
<input
value={form.password}
onChange={(e) =>
setForm((s) => ({ ...s, password: e.target.value }))
}
type={showPass ? "text" : "password"}
className="w-full border-b border-slate-300 py-2 pr-16 outline-none focus:border-slate-600"
/>
<button
type="button"
onClick={() => setShowPass((v) => !v)}
className="absolute right-8 top-1/2 -translate-y-1/2 text-slate-500"
>
{showPass ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
<Lock
className="absolute right-1 top-1/2 -translate-y-1/2 text-slate-500"
size={18}
/>
</div>
{errors.password && (
<p className="mt-1 text-xs text-red-600">
{errors.password}
</p>
)}
</div>
{/* Botón */}
<button
type="submit"
disabled={!canSubmit}
className="mt-2 w-full rounded-full bg-gradient-to-r from-[#4c5f9e] to-[#566bb8] py-2.5 text-white shadow-md flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
>
{loading ? (
<>
<Loader2 className="animate-spin" size={18} />
Entrando...
</>
) : (
"Iniciar sesión"
)}
</button>
</form>
</div> </div>
</div>
</section> {/* Contraseña */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Contraseña
</label>
<div className="relative">
<input
value={form.password}
onChange={(e) =>
setForm((s) => ({ ...s, password: e.target.value }))
}
type={showPass ? "text" : "password"}
placeholder="••••••••"
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-3 pr-20 text-slate-900 placeholder-slate-400 outline-none transition-all focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
/>
<button
type="button"
onClick={() => setShowPass((v) => !v)}
className="absolute right-11 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
>
{showPass ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
<Lock
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400"
size={18}
/>
</div>
{errors.password && (
<p className="mt-1.5 text-xs text-red-600">{errors.password}</p>
)}
</div>
{/* Botón */}
<button
type="submit"
disabled={!canSubmit}
className="w-full rounded-lg bg-blue-600 py-3 font-semibold text-white shadow-lg transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 active:scale-[0.98]"
>
{loading ? (
<>
<Loader2 className="animate-spin" size={20} />
Ingresando...
</>
) : (
"Ingresar"
)}
</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,12 +13,12 @@ export default function SHMetersPage() {
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">SH-METERS</h1> <h1 className="text-2xl font-bold text-gray-800 dark:text-white">SH-METERS</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">Conector para medidores SH</p> <p className="text-sm text-gray-500 dark:text-zinc-400">Conector para medidores SH</p>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <div className="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 p-6">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
@@ -26,10 +26,10 @@ export default function SHMetersPage() {
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<Radio className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" /> <Radio className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2"> <h3 className="text-lg font-medium text-gray-700 dark:text-zinc-200 mb-2">
Conector SH-METERS Conector SH-METERS
</h3> </h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto"> <p className="text-gray-500 dark:text-zinc-400 max-w-md mx-auto">
Configuracion e integracion con medidores SH. Configuracion e integracion con medidores SH.
Esta seccion esta en desarrollo. Esta seccion esta en desarrollo.
</p> </p>

View File

@@ -13,12 +13,12 @@ export default function TTSPage() {
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">TTS</h1> <h1 className="text-2xl font-bold text-gray-800 dark:text-white">TTS</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">The Things Stack - Integracion LoRaWAN</p> <p className="text-sm text-gray-500 dark:text-zinc-400">The Things Stack - Integracion LoRaWAN</p>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <div className="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 p-6">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
@@ -26,10 +26,10 @@ export default function TTSPage() {
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<Wifi className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" /> <Wifi className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2"> <h3 className="text-lg font-medium text-gray-700 dark:text-zinc-200 mb-2">
Conector TTS (The Things Stack) Conector TTS (The Things Stack)
</h3> </h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto"> <p className="text-gray-500 dark:text-zinc-400 max-w-md mx-auto">
Configuracion e integracion con The Things Stack para dispositivos LoRaWAN. Configuracion e integracion con The Things Stack para dispositivos LoRaWAN.
Esta seccion esta en desarrollo. Esta seccion esta en desarrollo.
</p> </p>

View File

@@ -13,12 +13,12 @@ export default function XMetersPage() {
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">XMETERS</h1> <h1 className="text-2xl font-bold text-gray-800 dark:text-white">XMETERS</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">Conector para medidores X</p> <p className="text-sm text-gray-500 dark:text-zinc-400">Conector para medidores X</p>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <div className="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 p-6">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
@@ -26,10 +26,10 @@ export default function XMetersPage() {
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<Gauge className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" /> <Gauge className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2"> <h3 className="text-lg font-medium text-gray-700 dark:text-zinc-200 mb-2">
Conector XMETERS Conector XMETERS
</h3> </h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto"> <p className="text-gray-500 dark:text-zinc-400 max-w-md mx-auto">
Configuracion e integracion con medidores X. Configuracion e integracion con medidores X.
Esta seccion esta en desarrollo. Esta seccion esta en desarrollo.
</p> </p>