Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# Supabase Auth con Google
|
||||||
|
# Crea un archivo .env.local y coloca tus valores reales.
|
||||||
|
VITE_SUPABASE_URL="https://TU-PROYECTO.supabase.co"
|
||||||
|
VITE_SUPABASE_ANON_KEY="TU_SUPABASE_ANON_KEY"
|
||||||
|
|
||||||
|
# URL pública esperada para producción:
|
||||||
|
# https://digitalcompass.agency/cruce-cuentas/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Cruce de Cuentas GLM
|
||||||
|
|
||||||
|
Portal centralizado para seleccionar el módulo de cruce de cuentas por país.
|
||||||
|
|
||||||
|
## Configuración local
|
||||||
|
|
||||||
|
1. Instala dependencias:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Crea un archivo `.env.local` en la raíz del proyecto:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_SUPABASE_URL="https://TU-PROYECTO.supabase.co"
|
||||||
|
VITE_SUPABASE_ANON_KEY="TU_SUPABASE_ANON_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ejecuta el proyecto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceso permitido
|
||||||
|
|
||||||
|
La app está limitada en frontend a estos correos:
|
||||||
|
|
||||||
|
- ymadera@gomezleemarketing.com
|
||||||
|
- iaracena@gomezleemarketing.com
|
||||||
|
|
||||||
|
Cualquier otro correo que inicie sesión con Google se cierra automáticamente y no entra al portal.
|
||||||
|
|
||||||
|
## Base de Vite
|
||||||
|
|
||||||
|
El proyecto ya tiene configurado en `vite.config.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
base: "/cruce-cuentas/"
|
||||||
|
```
|
||||||
|
|
||||||
|
URL de producción esperada:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://digitalcompass.agency/cruce-cuentas/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supabase Auth
|
||||||
|
|
||||||
|
En Supabase Auth / URL Configuration agrega:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Site URL: https://digitalcompass.agency/cruce-cuentas/
|
||||||
|
Redirect URLs:
|
||||||
|
https://digitalcompass.agency/cruce-cuentas/
|
||||||
|
http://localhost:3000/cruce-cuentas/
|
||||||
|
```
|
||||||
|
|
||||||
|
También debes tener Google como provider activo en Supabase Auth.
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>My Google AI Studio App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Cruce de Cuentas GLM",
|
||||||
|
"description": "Portal centralizado de Cruce de Cuentas GLM para la conciliación de cuentas regionales.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||||
|
}
|
||||||
Generated
+4305
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "cruce-de-cuentas-glm",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rm -rf dist server.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^2.4.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"motion": "^12.23.24",
|
||||||
|
"@supabase/supabase-js": "^2.77.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"@types/express": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { Session } from "@supabase/supabase-js";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { LoginScreen } from "./components/LoginScreen";
|
||||||
|
import { PortalPrincipal } from "./components/PortalPrincipal";
|
||||||
|
import { ALLOWED_EMAILS, isAllowedEmail } from "./lib/auth";
|
||||||
|
import { isSupabaseConfigured, supabase } from "./lib/supabase";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const [isLoadingSession, setIsLoadingSession] = useState(true);
|
||||||
|
const [authError, setAuthError] = useState("");
|
||||||
|
|
||||||
|
const validateAndSetSession = async (currentSession: Session | null) => {
|
||||||
|
const email = currentSession?.user?.email;
|
||||||
|
|
||||||
|
if (!currentSession) {
|
||||||
|
setSession(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllowedEmail(email)) {
|
||||||
|
setSession(null);
|
||||||
|
setAuthError(
|
||||||
|
"Este correo no tiene acceso al portal. Usa ymadera@gomezleemarketing.com o iaracena@gomezleemarketing.com."
|
||||||
|
);
|
||||||
|
await supabase?.auth.signOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthError("");
|
||||||
|
setSession(currentSession);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSupabaseConfigured || !supabase) {
|
||||||
|
setAuthError("Falta configurar Supabase en el archivo .env.local.");
|
||||||
|
setIsLoadingSession(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
supabase.auth.getSession().then(async ({ data, error }) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setAuthError("No se pudo validar la sesión. Intenta iniciar sesión nuevamente.");
|
||||||
|
setSession(null);
|
||||||
|
} else {
|
||||||
|
await validateAndSetSession(data.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingSession(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: listener } = supabase.auth.onAuthStateChange(async (_event, currentSession) => {
|
||||||
|
await validateAndSetSession(currentSession);
|
||||||
|
setIsLoadingSession(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
listener.subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
setAuthError("");
|
||||||
|
|
||||||
|
if (!isSupabaseConfigured || !supabase) {
|
||||||
|
setAuthError("Falta configurar VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY en .env.local.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectTo = `${window.location.origin}${import.meta.env.BASE_URL}`;
|
||||||
|
|
||||||
|
const { error } = await supabase.auth.signInWithOAuth({
|
||||||
|
provider: "google",
|
||||||
|
options: {
|
||||||
|
redirectTo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setAuthError("No se pudo iniciar sesión con Google. Revisa la configuración de Supabase Auth.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
setAuthError("");
|
||||||
|
await supabase?.auth.signOut();
|
||||||
|
setSession(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingSession) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#f5f3f3] flex items-center justify-center text-[#365c71] text-sm font-semibold">
|
||||||
|
Validando sesión...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoggedIn = Boolean(session && isAllowedEmail(session.user.email));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#fbf9f8]" id="app-container">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!isLoggedIn ? (
|
||||||
|
<motion.div
|
||||||
|
key="login"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<LoginScreen
|
||||||
|
onLogin={handleLogin}
|
||||||
|
authError={authError}
|
||||||
|
isSupabaseConfigured={isSupabaseConfigured}
|
||||||
|
allowedEmails={ALLOWED_EMAILS}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="portal"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<PortalPrincipal onLogout={handleLogout} userEmail={session?.user.email ?? ""} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { motion } from "motion/react";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { Country } from "../data";
|
||||||
|
|
||||||
|
interface CountryCardProps {
|
||||||
|
country: Country;
|
||||||
|
onSelect: (country: Country) => void;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CountryCard({ country, onSelect }: CountryCardProps) {
|
||||||
|
return (
|
||||||
|
<motion.article
|
||||||
|
whileHover={{ scale: 1.01, transition: { duration: 0.15 } }}
|
||||||
|
className="bg-white border border-[#D0D0D0] border-l-4 border-l-[#6CC24A] p-6 flex flex-col justify-between min-h-[220px] rounded-none shadow-none hover:bg-[#F5F5F5] transition-colors group"
|
||||||
|
id={`card-${country.code.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* Country Flag Container */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<img
|
||||||
|
alt={`Bandera de ${country.name}`}
|
||||||
|
className="w-12 h-8 object-cover border border-[#D0D0D0] rounded-none"
|
||||||
|
src={country.flagUrl}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Country Name */}
|
||||||
|
<h3 className="text-xl font-bold text-[#4F758B] mb-2" id={`title-${country.code.toLowerCase()}`}>
|
||||||
|
{country.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => onSelect(country)}
|
||||||
|
className="bg-[#4F758B] text-white text-xs font-semibold py-3 px-4 w-full rounded-none mt-6 hover:bg-[#365C71] active:bg-[#234b60] transition-colors text-left flex justify-between items-center group-hover:bg-[#365C71] cursor-pointer focus:outline-none"
|
||||||
|
title={`Cruce de Cuentas GLM - ${country.code}`}
|
||||||
|
id={`btn-cruce-${country.code.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
<span>Cruce de Cuentas GLM - {country.code}</span>
|
||||||
|
<ArrowRight className="w-4 h-4 text-white group-hover:translate-x-1 transition-transform" />
|
||||||
|
</button>
|
||||||
|
</motion.article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { IMAGES } from "../data";
|
||||||
|
|
||||||
|
interface LoginScreenProps {
|
||||||
|
onLogin: () => Promise<void> | void;
|
||||||
|
authError?: string;
|
||||||
|
isSupabaseConfigured?: boolean;
|
||||||
|
allowedEmails?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginScreen({
|
||||||
|
onLogin,
|
||||||
|
authError,
|
||||||
|
isSupabaseConfigured = true,
|
||||||
|
allowedEmails = [],
|
||||||
|
}: LoginScreenProps) {
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
|
||||||
|
const handleGoogleLogin = async () => {
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
try {
|
||||||
|
await onLogin();
|
||||||
|
} finally {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full flex items-center justify-center p-4 md:p-8 bg-[#f5f3f3]" id="login-screen-bg">
|
||||||
|
{/* Main Login Card container */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
|
className="w-full max-w-[480px] bg-white rounded-xl p-8 md:p-12 shadow-lg flex flex-col items-center"
|
||||||
|
id="login-card"
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-col items-center text-center space-y-8 py-4">
|
||||||
|
|
||||||
|
{/* Company Brand Logo */}
|
||||||
|
<div className="w-full flex justify-center pb-2" id="login-brand-logo-container">
|
||||||
|
<img
|
||||||
|
alt="GLM Logo"
|
||||||
|
className="h-auto max-w-[250px]"
|
||||||
|
src={IMAGES.loginLogo}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div className="space-y-1 w-full">
|
||||||
|
<h1 className="text-[32px] leading-[1.2] font-bold text-[#4f758b]" id="login-h1-title">
|
||||||
|
Cruce de Cuentas GLM
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Google Button */}
|
||||||
|
<div className="w-full pt-4 pb-2 space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={handleGoogleLogin}
|
||||||
|
disabled={isLoggingIn || !isSupabaseConfigured}
|
||||||
|
className="w-full flex items-center justify-center gap-3 bg-[#6CC24A] hover:bg-[#5bb03a] text-white py-3 px-4 transition-all text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6CC24A] focus:ring-offset-2 cursor-pointer shadow-none disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
id="google-signin-btn"
|
||||||
|
>
|
||||||
|
{isLoggingIn ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 text-white animate-spin" />
|
||||||
|
<span>Iniciando sesión...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Reference G white Logo */}
|
||||||
|
<svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-[16px] font-bold text-white">Continuar con Google</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{authError ? (
|
||||||
|
<p className="text-[12px] leading-[1.45] text-red-600 bg-red-50 border border-red-100 rounded-lg p-3">
|
||||||
|
{authError}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Small text footer */}
|
||||||
|
<div className="w-full pt-4 border-t border-[#D0D0D0] text-center" id="login-footer">
|
||||||
|
<p className="text-[13px] leading-[1.4] text-slate-500 font-normal">
|
||||||
|
Acceso restringido. Uso exclusivo para personal autorizado de GLM.
|
||||||
|
</p>
|
||||||
|
{allowedEmails.length > 0 ? (
|
||||||
|
<p className="text-[11px] leading-[1.4] text-slate-400 font-normal mt-2">
|
||||||
|
Acceso habilitado solo para usuarios autorizados.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, ShieldAlert, ArrowLeft, Layers, Landmark, Info, CheckCircle2, ChevronRight } from "lucide-react";
|
||||||
|
import { Country } from "../data";
|
||||||
|
|
||||||
|
interface NotificationModalProps {
|
||||||
|
country: Country | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationModal({ country, onClose }: NotificationModalProps) {
|
||||||
|
if (!country) return null;
|
||||||
|
|
||||||
|
const isGuatemala = country.code === "GT";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop overlay */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal Content Card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
transition={{ type: "spring", duration: 0.4 }}
|
||||||
|
className={`relative w-full max-w-lg bg-white rounded-xl shadow-2xl overflow-hidden border-t-4 ${isGuatemala ? 'border-[#6CC24A]' : 'border-slate-300'}`}
|
||||||
|
id={`modal-${country.code.toLowerCase()}`}
|
||||||
|
>
|
||||||
|
{/* Close button top right */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full transition-colors focus:outline-none"
|
||||||
|
title="Cerrar"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Header Banner info */}
|
||||||
|
<div className="bg-slate-50 p-6 border-b border-slate-100 flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-lg overflow-hidden shrink-0 border border-slate-200">
|
||||||
|
<img
|
||||||
|
src={country.flagUrl}
|
||||||
|
alt={`Bandera de ${country.name}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={`text-[10px] font-bold tracking-wider px-2.5 py-1 rounded-full uppercase ${isGuatemala ? 'text-[#6CC24A] bg-[#6CC24A]/10' : 'text-slate-500 bg-slate-100'}`}>
|
||||||
|
{isGuatemala ? 'Iniciado' : 'No Iniciado'}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-xl font-bold text-slate-800 leading-tight mt-1">
|
||||||
|
{country.name} ({country.code})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{isGuatemala ? (
|
||||||
|
// Guatemala Particular Content
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex gap-3 bg-emerald-50/50 rounded-lg p-4 border border-emerald-100">
|
||||||
|
<CheckCircle2 className="w-6 h-6 text-[#6CC24A] shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-slate-800 text-sm">
|
||||||
|
Módulo en Operación Inicial
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-slate-600 mt-1 leading-relaxed">
|
||||||
|
El sistema para Guatemala ha comenzado su fase técnica y de parametrización operativa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h5 className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||||||
|
Siguientes pasos en desarrollo:
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
<div className="flex items-center gap-3 p-3.5 bg-yellow-50/40 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="p-2 bg-yellow-100/50 text-amber-700 rounded-md">
|
||||||
|
<Layers className="w-5 h-5 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs font-bold text-slate-800">Falta el estándar de nómina</h6>
|
||||||
|
<p className="text-[11px] text-slate-600 mt-0.5">Adaptación de los campos reglamentarios locales.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3.5 bg-yellow-50/40 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="p-2 bg-yellow-100/50 text-amber-700 rounded-md">
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs font-bold text-slate-800">Integrar al flujo de n8n</h6>
|
||||||
|
<p className="text-[11px] text-slate-600 mt-0.5">Conexión de webhooks y procesamiento de conciliación en lote.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Pending / No Iniciado Content for other countries
|
||||||
|
<div className="space-y-4 py-4 text-center">
|
||||||
|
<div className="w-16 h-16 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center mx-auto text-slate-400">
|
||||||
|
<ShieldAlert className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 max-w-sm mx-auto">
|
||||||
|
<h4 className="font-bold text-slate-800 text-base">
|
||||||
|
Módulo No Iniciado
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
Aún no se ha iniciado el proceso operativo ni la conciliación para {country.name}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons footer */}
|
||||||
|
<div className="bg-slate-50 px-6 py-4 border-t border-slate-100 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex items-center gap-2 px-5 py-2.5 bg-[#4F758B] hover:bg-[#365C71] text-white text-xs font-semibold rounded-lg shadow-sm transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" /> Entendido, Volver
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { LogOut, User } from "lucide-react";
|
||||||
|
import { COUNTRIES, Country, IMAGES } from "../data";
|
||||||
|
import { CountryCard } from "./CountryCard";
|
||||||
|
import { NotificationModal } from "./NotificationModal";
|
||||||
|
|
||||||
|
interface PortalPrincipalProps {
|
||||||
|
onLogout: () => void;
|
||||||
|
userEmail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortalPrincipal({ onLogout, userEmail }: PortalPrincipalProps) {
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState<Country | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[#fbf9f8] text-[#1b1c1c] font-sans min-h-screen flex flex-col" id="portal-root">
|
||||||
|
|
||||||
|
{/* Main Content Canvas */}
|
||||||
|
<main className="flex-grow pb-16 px-4 md:px-12 py-8 max-w-7xl w-full mx-auto" id="portal-main">
|
||||||
|
|
||||||
|
{/* Top Control Bar with Logout and User Profile */}
|
||||||
|
<div className="flex justify-end mb-8 gap-2" id="portal-controls">
|
||||||
|
<button
|
||||||
|
onClick={onLogout}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-[#4F758B] hover:bg-[#365C71] text-white transition-all text-xs font-semibold rounded-lg shadow-sm cursor-pointer border border-[#4F758B]/10"
|
||||||
|
title="Cerrar sesión"
|
||||||
|
id="logout-btn"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4 text-white" />
|
||||||
|
<span className="text-white">Cerrar sesión</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-[#4F758B] border border-slate-300" title={userEmail ? `Sesión activa: ${userEmail}` : "Usuario de GomezLee Marketing"}>
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Section (Asymmetric) */}
|
||||||
|
<section className="grid grid-cols-1 md:grid-cols-12 gap-8 my-16">
|
||||||
|
<div className="md:col-span-8 flex flex-col justify-center">
|
||||||
|
<h1 className="text-[48px] leading-[1.1] tracking-tight font-bold text-[#365c71] mb-4">
|
||||||
|
Cruce de Cuentas GLM
|
||||||
|
</h1>
|
||||||
|
<p className="text-[16px] leading-[1.6] text-slate-600 max-w-2xl">
|
||||||
|
Plataforma centralizada para la conciliación de cuentas a nivel regional. Seleccione un país para comenzar el proceso operativo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-4 hidden md:flex items-center justify-end">
|
||||||
|
<div className="w-full flex items-center justify-end">
|
||||||
|
<img
|
||||||
|
alt="GLM Logo"
|
||||||
|
className="max-w-full h-auto object-contain"
|
||||||
|
src={IMAGES.portalLogo}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-1 md:col-span-12 border-t border-[#6CC24A] mt-8"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Country Grid Section */}
|
||||||
|
<section className="mb-24" id="portales-por-pais">
|
||||||
|
<h2 className="text-[28px] md:text-[32px] leading-[1.2] font-bold text-[#365c71] mb-8">
|
||||||
|
Portales por país
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{COUNTRIES.map((country) => (
|
||||||
|
<CountryCard
|
||||||
|
key={country.code}
|
||||||
|
country={country}
|
||||||
|
onSelect={setSelectedCountry}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-slate-200 py-6 text-center text-xs text-slate-400 font-medium">
|
||||||
|
<p>© {new Date().getFullYear()} GomezLee Marketing. Todos los derechos reservados.</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{/* Interactive Country Modal */}
|
||||||
|
<NotificationModal
|
||||||
|
country={selectedCountry}
|
||||||
|
onClose={() => setSelectedCountry(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
export interface Country {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
flagUrl: string;
|
||||||
|
reconciliationStatus?: 'pending' | 'success' | 'checking';
|
||||||
|
lastActivity?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COUNTRIES: Country[] = [
|
||||||
|
{
|
||||||
|
name: "República Dominicana",
|
||||||
|
code: "RD",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuAbFgio9fFr7HYBSPjeTfwPI8GFXnGp_3skeWFL_baovcRdG3WZ2OoKvEwCcBulfJu6jkY82lNHkpDH5qKnpNmSG0cC8vp1okMWse2WB0iE3_ZGE9JgyFh52HKKjTogPQDRFUCPe5yY1hZ9mK47vc0I26flb3J-eBfaB9vw00SI4DNPzJzV7eyvLoljWKE-2cX6cFATt0eC-QrHh6COewzbZFLceN_9dXCnVZkllHX5EVsgmSh1zyWmaWZ7bJ1ytTacvq3a2YkSCFDh",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 2 horas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "México",
|
||||||
|
code: "MX",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuBDh5QV_WHLXY2gw69AgzFahKNRfHAvxfzAU911jSu-k4GFhR-5lQpXjokfCyn-I2k6wH2mBrEir6PulF8gdAmXQvmC3RKrhkxwt1OneWHyyE3Rl8SNuWpabySiwuCR0CQuKkvVA7fTtugNkGkcuolxod04knCbFXeVGDDYp7E7bkv8OPMzUxoybYFhTkeVk6FuJL9eVkV2CHEnW0oYSyIsyQ2WvVPxmGtvZelV3yk-fTNsrjGFdGrUNAj-0LeE3yP0OkazNrOg0IKI",
|
||||||
|
reconciliationStatus: "pending",
|
||||||
|
lastActivity: "Ayer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Guatemala",
|
||||||
|
code: "GT",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuAHvkRR8pLZXEo5FCWvaPHE8oWO1KClQ013s182tamIM4BHJT2h4vE3Q3iwXreUhLBwBJ2NO8cu5YjbkEwRHJqrC2pCiSbAc7iVY9mZGI3EsYzJMiLyuPIjb69MXgEgGoYlLUSzTof2Z02Ydorsb9bQMbzvUCThuzJ62q6tjLW8zNxmgF8_vKZXlE2FUMZup_RLrk5rFfxbBLUKyNZTffWoDc0gCCRmszLpX1eYl9GK_440rwegPXPDfFWQmAraQyukFepcalVEVF1F",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 3 días"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "El Salvador",
|
||||||
|
code: "SV",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuBnMCkUdotqwCWah5GJY6we6HN7JohrfhMUP4OWviKUFHkrzYTEeu-u0_YNZ6rSpwi7uUzwr8CrduO2yUNNUH4BIpBg9z1cBMLcCBRDmw_0_p09vVy0dYQReG_j2xfMzTGVItXO4VAiyfHacxcPyn99m7NKhdFQzGWvPSYuAg9G97OnplzbH41s8SzQVj7Brm-F7XNqbXPgVuOEBjaiYalGEf7Z-RexyMznTH9l_f67UcK5uCmhLAwQOvm3eadnjPls7HKT1p76IKwg",
|
||||||
|
reconciliationStatus: "pending",
|
||||||
|
lastActivity: "Hace 1 semana"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Honduras",
|
||||||
|
code: "HN",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCQ7Vzvmn3kzPTSJDG2nwUasogs2adICwKFaVrEK2b26jgzdXuG6vQ2CJIgAyM0muR0_7_oQo4cmg_VQmJUBw_B4c_uusqz_WBN1dXz2n25o2CkHRruystQgUDTrMMRipwDqCaARGG8g838f9Dn_N2o9oJPl39j0QOivdQ6g5X0UFxTa97zx-xu8ABbtk2sBVCeR2yX47C5GBDCKpYhD7T8g-iw0BQ0YvgsE7GlezpZQq4dJHiq_Hm4dvcY-v2DKRqkVqe9Q3vv0ggP",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 5 días"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nicaragua",
|
||||||
|
code: "NI",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCnoSqAalLZksu58PPPnEOHaGz505p_P4KaQP8QX2lucGZmx6a_BtodhrbKW1lIs9NRh7d9ubueOBWoiRuno1kruz7buM2u6HUb1X4qgXTfek_-E5d2DbXMSVE5rjWCFIPWDJYduJrdM2Z1JxEHfu3uqwf8iG9mLC2NE0lLGKnLCPBsu6gWEA-GmsL4syXh56BpXYs7fVFyTVJ-u0kM6P1-XnyOcFBkjIqbq8DsS9jplkfEUf4mm6eVV3kGaP7EkbtC8reV-wLDAGex",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 2 semanas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Costa Rica",
|
||||||
|
code: "CR",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCTOyc-REp3byTH-hJwskzWobb1-XtP1dZfecPSifYuV8qGuFLOMNaAbOj7131fS5Sf4UOR2QJqNQLm90bvEobvU4WBupNuyPRZ_Nj6CYfv2HPedNvMHdqAVmuSoV492KMdGnHYdOPKP074MfDxMwTx4BaDYw8KdBtZI2TKLPj2daWugYxFArTHjP-g_MshvEjYQiY9w7AdPODBRj_QVVizM2hjuuvwmsxRjzjhSjZ1D4TFoDCKYfk9udSbYCWF3qkzYRQvLNXvrpZq",
|
||||||
|
reconciliationStatus: "pending",
|
||||||
|
lastActivity: "Hace 6 días"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Panamá",
|
||||||
|
code: "PA",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCRfO8gX-PWWRyjm7mQBFPY-l8vC_NVJnZ1FAYJrdm2QZvqssC6XZ2zbAe1vGfst9lOOJghcis6V5ibunmh23oCl6jYp6G-URvUth0V0hzuxQYoQFfoe0OFY1n-eTa6pCHDLPm6Ko7T9yduVIulm4PTQToAHzB83k7d_2-P79EjguS4A0nqHzQFSu0SUbwlYb0JwtWG3JtBYF6cDIeMcWPyixRGSmbPyNrVAo1TP57dVZ6KbxRFxzwOY-yWsh-v77K_AJ13XsKZqDhL",
|
||||||
|
reconciliationStatus: "checking",
|
||||||
|
lastActivity: "En progreso"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Colombia",
|
||||||
|
code: "CO",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuBG_GcA4hQ0Y6W9RrG214QNcVA7RLqVdGEHC3r62z-mRBJjlkfiRTqiRQpCYs7c8_JDnnq_83WquYhKdX2hbWZEdY1GqbsTm3qj8Sb4l0zBLYg8CTVU_uas1zPT4i0UjyLkPnRXLIAw9SMJu0v9uiuJr4PqLS8xGjf9Kh6hX47ivq2hGpKBUInK40SB7S_D8aULwsLVuEbLsgC7zKBaWuxrYRXHCccrFlGgILykf1Q36pqCySp7GX04mvvacPcBhD2E7Rei9DDIcxwb",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 4 horas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Puerto Rico",
|
||||||
|
code: "PR",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCeW3pYEOintyivpkLraB5zQjxT7QC4IKgBZ-vecQcE3n83pUbgcBt9_uuQQKFXWl-ObdTA3G5WSjIGIbJ3RwxfKWMgaYYu7qP1i_NWfwD7i8qsfD519cYLZSE4eUY2Le11cYAcUB8AL5mqbW_M5NSw2qQuG6Sy2RyX-4MB9U8BGENLYUosZgtGnNnLqy9SLhKi5axr5rRE_pY11xQ3SInUnTd84lsBNAZ1CGqoIXHelz-rBtSylIUQQiYwOhYf35g9SQMUwjkFYiBB",
|
||||||
|
reconciliationStatus: "pending",
|
||||||
|
lastActivity: "Hace 3 semanas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jamaica",
|
||||||
|
code: "JM",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuA_P_G_Y_37QIBSW3S1meV-z7oY9-a2H5BcMgye9Fl291icbi3xjuZJYDZw6gw1ba9khrdyplzuHh47Ekp8-SrbGtWVb4rKpeCRExyeN7u5yMLYRO4COUm6xeJH_oaArq2Jlm_9D_uSa0KZxRNpZzoazD5pQGD-NQ3I_Uir_Y92sLbdTdQUK1zs8iMfX7leTS-p0-huFuCIpfRPtm1NfoiLx7aUPcnv5D8lt8KDN9yf-sVVqAJ-qq5kb2QE00A3GIRKOnkj-Z_B7q5H",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Hace 10 días"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Trinidad y Tobago",
|
||||||
|
code: "TT",
|
||||||
|
flagUrl: "https://lh3.googleusercontent.com/aida-public/AB6AXuCiLtOuYmRWakq32f_gdadIAACMfjrXp2AcVhti893m3_SiZ_HsRf5cypDuqtxTrxMxRxmWxAOlMKS0SL-kccU_m5HM8oErMyAt4C_PCeUGxyIAij5ZRhdFS13_4IUMhVPlXGfutBtqekNEW1dTG1V4X9c6KEVzGrLOUl1sIVKJdWMnvGneEWDObhVK-b57DMHCazHgiQ0r5rQ_-flKe0O0ItxgDslabTXCCDIg2cIif7u5xtHUez7Pgb_aYL0w1y62cnG23SHKKHL6",
|
||||||
|
reconciliationStatus: "success",
|
||||||
|
lastActivity: "Ayer"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const IMAGES = {
|
||||||
|
loginLogo: "https://lh3.googleusercontent.com/aida-public/AB6AXuB3NT2YzcwdXGZinCF8VMS_mnwo7q8ocY9GQLEECB_Fxmo26w3htpicfjrAw3wFWnwBHo7Ew9QPgu8twLNHD9qDZnvGAO9TEy6zccaPqOzMQBZlOC0HcGr-U6xqwLdQrdnwTNpMrN7l1Innpjy9aHKJeV8puZN-KI2TRj_2x81mkQOLGCTplyBI95AWgjM18vtLlBFPPmOLwO2njvLd5n7REBV3tQlMFeAr55BLJtDCVgJ5N8PTbxMIlxgpRKMu4OS3JnQY01qwp2jY",
|
||||||
|
portalLogo: "https://lh3.googleusercontent.com/aida-public/AB6AXuDaWPVKf2WugmtyGs6I4JBK2NVDbejr_TrbMWKkDtFUqD_W-qvq3ZkpwYaBshUCgbUgo5AhuKvIWwat8hDL7IUjgSbChiYUCjgsuz7uy94EM0FpbPH5krRwHcv6K-uAqIs4uFAk4GCSLfJMTsGFHYydpl_yLtfWu0e1ULAJQPJ0I9HmHESmcc2kT7DOWr8lapGowWTI4OatRgcngo9ZmKKsAWaaJUy4Fi3Zk6U-4S2dKhsItIPg_PVaxqaDt7tfbCFHu7CY0db4BHCD"
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display: "Space Grotesk", sans-serif;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export const ALLOWED_EMAILS = [
|
||||||
|
"ymadera@gomezleemarketing.com",
|
||||||
|
"iaracena@gomezleemarketing.com",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function normalizeEmail(email?: string | null) {
|
||||||
|
return (email ?? "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAllowedEmail(email?: string | null) {
|
||||||
|
return ALLOWED_EMAILS.includes(normalizeEmail(email) as (typeof ALLOWED_EMAILS)[number]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string | undefined;
|
||||||
|
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined;
|
||||||
|
|
||||||
|
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseAnonKey);
|
||||||
|
|
||||||
|
export const supabase = isSupabaseConfigured
|
||||||
|
? createClient(supabaseUrl!, supabaseAnonKey!, {
|
||||||
|
auth: {
|
||||||
|
persistSession: true,
|
||||||
|
autoRefreshToken: true,
|
||||||
|
detectSessionInUrl: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: null;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {StrictMode} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import {defineConfig} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
base: "/cruce-cuentas/",
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||||
|
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||||
|
hmr: process.env.DISABLE_HMR !== 'true',
|
||||||
|
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||||
|
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user