Subir archivos a "src/pages"

This commit is contained in:
2026-05-07 18:50:04 +00:00
parent fa8b4dd946
commit 80bbe19d81
4 changed files with 734 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import ReCAPTCHA from 'react-google-recaptcha';
import { supabase } from '../lib/supabase';
import { useAuth } from '../contexts/AuthContext';
export default function Auth() {
const [isLogin, setIsLogin] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
const { session, isLoading: authLoading } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [nombre, setNombre] = useState('');
const [apellido, setApellido] = useState('');
const [alias, setAlias] = useState('');
const [celular, setCelular] = useState('');
const [fechaNacimiento, setFechaNacimiento] = useState('');
const [termsAccepted, setTermsAccepted] = useState(false);
const [captchaValue, setCaptchaValue] = useState<string | null>(null);
const recaptchaRef = useRef<ReCAPTCHA>(null);
useEffect(() => {
if (authLoading) return;
if (session) {
navigate('/dashboard');
}
}, [session, authLoading, navigate]);
if (authLoading || session) {
return null;
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!captchaValue) {
setError("Por favor, completa el reCAPTCHA.");
return;
}
if (!isLogin && password !== confirmPassword) {
setError("Las contraseñas no coinciden.");
return;
}
if (!isLogin && !termsAccepted) {
setError("Debes aceptar los términos y condiciones.");
return;
}
setLoading(true);
setError(null);
try {
if (isLogin) {
const { error: signInError } = await supabase.auth.signInWithPassword({ email, password });
if (signInError) throw signInError;
navigate('/dashboard');
} else {
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({ email, password });
if (signUpError) throw signUpError;
if (signUpData.user) {
const { error: rpcError } = await supabase.rpc('rpc_create_profile', {
p_id: signUpData.user.id,
p_nombre: nombre,
p_apellido: apellido,
p_alias: alias,
p_celular: celular,
p_fecha_nacimiento: fechaNacimiento
});
if (rpcError) {
console.error("Profile error:", rpcError);
throw new Error("No se pudo crear el perfil correctamente.");
}
}
navigate('/dashboard');
}
} catch (err: any) {
console.error(err);
setError(err.message || 'Ocurrió un error. Por favor, intenta de nuevo.');
recaptchaRef.current?.reset();
setCaptchaValue(null);
} finally {
setLoading(false);
}
};
return (
<div className="flex-1 flex flex-col animate-in fade-in duration-300 min-h-[calc(100vh-80px)]">
{/* Dynamic Background */}
<div className="absolute inset-0 z-0 flex overflow-hidden">
<div className="w-1/2 bg-gradient-to-br from-brand-red to-brand-dark-blue"></div>
<div className="w-1/2 bg-gradient-to-bl from-brand-blue to-brand-dark-blue"></div>
{/* Abstract Texture Overlay */}
<div className="absolute inset-0 z-0 opacity-30 mix-blend-overlay">
<img
src={`${import.meta.env.BASE_URL}images/auth-bg.jpg`}
alt="Texture"
className="w-full h-full object-cover"
onError={(e) => { e.currentTarget.src = "https://images.unsplash.com/photo-1557672172-298e090bd0f1?q=80&w=2000&auto=format&fit=crop" }} /* Fallback while they don't upload their image */
/>
</div>
</div>
<div className="relative z-10 flex-1 flex items-center justify-center p-4 py-12">
<div className="bg-brand-dark-blue/95 backdrop-blur-md w-full max-w-lg rounded-3xl p-8 md:p-10 shadow-2xl overflow-hidden relative border border-white/5">
{/* White top right circle embellishment */}
<div className="absolute -top-16 -right-16 w-48 h-48 bg-white rounded-full opacity-100"></div>
<h1 className="font-display italic text-4xl text-brand-yellow mb-8 tracking-wider drop-shadow-sm text-center relative z-10">
{isLogin ? 'LOGIN' : 'REGISTRO'}
</h1>
{error && (
<div className="bg-red-500/10 text-red-300 p-4 rounded-xl mb-6 text-sm border border-red-500/20 text-center font-medium relative z-10">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-5 relative z-10">
{!isLogin && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Nombre</label>
<input type="text" required value={nombre} onChange={(e) => setNombre(e.target.value)} placeholder="Nombre" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Apellido</label>
<input type="text" required value={apellido} onChange={(e) => setApellido(e.target.value)} placeholder="Apellido" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Correo</label>
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="correo@ejemplo.com" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Contraseña</label>
<input type="password" required value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Contraseña" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Confirmar contraseña</label>
<input type="password" required value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} placeholder="Contraseña" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Alias</label>
<input type="text" required value={alias} onChange={(e) => setAlias(e.target.value)} placeholder="Alias" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Celular</label>
<input type="tel" required value={celular} onChange={(e) => setCelular(e.target.value)} placeholder="+507 0000-0000" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-bold text-brand-yellow">Fecha de nacimiento</label>
<input type="date" required value={fechaNacimiento} onChange={(e) => setFechaNacimiento(e.target.value)} className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" />
</div>
<div className="flex items-start gap-3 mt-2">
<input type="checkbox" id="terms" required checked={termsAccepted} onChange={(e) => setTermsAccepted(e.target.checked)} className="mt-1 w-4 h-4 rounded text-brand-blue" />
<label htmlFor="terms" className="text-xs text-white/80 leading-tight">
Acepto los términos y condiciones del programa de lealtad y autorizo el uso de mis datos personales conforme a la política de privacidad.
</label>
</div>
</>
)}
{isLogin && (
<>
<div className="flex flex-col gap-1">
<label className="text-base font-bold text-brand-yellow">Correo</label>
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Correo electrónico" className="w-full bg-white text-gray-900 rounded-xl px-4 py-4 outline-none text-base" />
</div>
<div className="flex flex-col gap-1 mt-2">
<label className="text-base font-bold text-brand-yellow">Contraseña</label>
<input type="password" required value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Contraseña" className="w-full bg-white text-gray-900 rounded-xl px-4 py-4 outline-none text-base" />
<div className="text-right mt-1">
<a href="#" className="text-white text-sm hover:underline">¿Olvidaste tu contraseña?</a>
</div>
</div>
</>
)}
<div className="flex justify-center mt-2">
<ReCAPTCHA
ref={recaptchaRef}
sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY || '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'}
onChange={(value) => setCaptchaValue(value)}
theme="light"
/>
</div>
<button
type="submit"
disabled={loading}
className="mt-6 w-full bg-brand-yellow text-brand-blue font-bold text-xl py-4 rounded-full hover:bg-brand-yellow/90 active:scale-[0.98] transition-all flex items-center justify-center gap-2 shadow-[0_0_20px_rgba(255,201,0,0.5)] disabled:opacity-70 disabled:pointer-events-none"
>
{loading ? <Loader2 className="animate-spin" /> : (isLogin ? 'Login' : 'Crear cuenta')}
</button>
</form>
<div className="mt-8 text-center relative z-10 text-white/80 shrink-0 flex items-center justify-center gap-1">
{isLogin ? (
<>¿No tienes cuenta? <button type="button" onClick={() => { setIsLogin(false); setError(null); recaptchaRef.current?.reset(); setCaptchaValue(null); }} className="text-white font-bold underline decoration-white/50 hover:decoration-white">Regístrate</button></>
) : (
<>¿Ya tienes cuenta? <button type="button" onClick={() => { setIsLogin(true); setError(null); recaptchaRef.current?.reset(); setCaptchaValue(null); }} className="text-white font-bold underline decoration-white/50 hover:decoration-white">Iniciar Sesión</button></>
)}
</div>
</div>
</div>
</div>
);
}
+190
View File
@@ -0,0 +1,190 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../lib/supabase';
import { useAuth } from '../contexts/AuthContext';
import { Trophy, Loader2, Check, Clock, X as XIcon } from 'lucide-react';
interface LeaderboardEntry {
is_current_user: boolean;
nombre: string;
puntos: number;
}
interface DashboardData {
user: {
first_name: string;
last_name: string;
initials: string;
};
total_points: number;
leaderboard: LeaderboardEntry[];
}
export default function Dashboard() {
const { user, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (authLoading) return;
if (!user) {
navigate('/auth');
return;
}
const fetchDashboard = async () => {
let isSuccess = false;
try {
const { data: dbData, error: dbError } = await supabase.rpc('rpc_get_dashboard', { p_user_id: user.id });
if (dbError) throw dbError;
const parsedData = Array.isArray(dbData) ? dbData[0] : dbData;
if (parsedData && parsedData.user) {
setData(parsedData);
isSuccess = true;
return;
}
} catch (err: any) {
// Silently catch the error to fall back to visual demonstration
} finally {
// Fallback for visual demonstration as requested
if (!isSuccess) {
setData({
user: { first_name: 'MARCELA', last_name: 'GIRALDO', initials: 'MG' },
total_points: 340,
leaderboard: [
{ is_current_user: false, nombre: 'CARLOS P.', puntos: 500 },
{ is_current_user: true, nombre: 'MARCELA G.', puntos: 340 },
{ is_current_user: false, nombre: 'JUAN M.', puntos: 200 }
]
});
}
setLoading(false);
}
};
fetchDashboard();
}, [user, navigate]);
if (loading) {
return (
<div className="flex-1 flex flex-col items-center justify-center w-full relative min-h-[500px]">
<div className="absolute inset-0 bg-brand-dark-blue flex items-center justify-center z-50">
<Loader2 className="animate-spin text-brand-yellow w-12 h-12" />
</div>
</div>
);
}
if (!data) return null;
const initial = data.user.initials || 'US';
const fullName = `${data.user.first_name} ${data.user.last_name}`;
const userRankIndex = data.leaderboard.findIndex(entry => entry.is_current_user);
const position = userRankIndex !== -1 ? userRankIndex + 1 : '-';
return (
<div className="flex-1 relative w-full overflow-hidden bg-brand-dark-blue flex flex-col">
{/* Image Background */}
<div className="absolute inset-0 z-0 overflow-hidden pointer-events-none">
<img
src={`${import.meta.env.BASE_URL}images/bg-perfil.png`}
alt="Background"
className="w-full h-full object-cover object-center"
/>
</div>
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 py-8 flex flex-col items-start min-h-screen">
{/* Header Profile */}
<div className="flex items-center gap-4 mb-8">
<div className="w-16 h-16 rounded-full bg-brand-blue text-brand-yellow font-display text-2xl flex items-center justify-center flex-shrink-0 border-4 border-brand-yellow shadow-lg italic">
{initial}
</div>
<h1 className="font-display italic text-4xl text-white drop-shadow-md uppercase tracking-wide">
{fullName}
</h1>
</div>
<div className="flex flex-col lg:flex-row gap-6 w-full">
{/* Left Column (Stats) */}
<div className="flex flex-col gap-6 w-full lg:w-[45%] xl:w-[40%]">
{/* Puntos Card */}
<div className="bg-brand-dark-blue/95 border border-brand-yellow/50 rounded-[2rem] p-8 shadow-2xl relative overflow-hidden text-brand-dark-blue">
{/* Star decorations */}
<div className="absolute top-4 right-8 text-brand-yellow">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg>
</div>
<div className="absolute top-12 right-24 text-brand-yellow scale-50 opacity-80">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg>
</div>
<div className="absolute top-20 right-6 text-brand-yellow scale-75 opacity-90">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg>
</div>
<h2 className="font-display text-brand-yellow text-xl uppercase tracking-wider mb-1">Puntos Acumulados</h2>
<div className="font-display text-white text-6xl md:text-7xl mb-2 flex items-baseline gap-2">
{data.total_points} <span className="text-3xl text-white font-normal lowercase tracking-wide">puntos</span>
</div>
</div>
{/* Posicion Card */}
<div className="bg-brand-cream rounded-[2rem] p-6 shadow-xl flex items-center justify-center gap-6 text-brand-dark-blue">
<Trophy className="text-brand-blue w-16 h-16 shrink-0" strokeWidth={1.5} />
<div className="flex flex-col">
<h3 className="font-display text-2xl uppercase tracking-wide text-brand-blue">Posición en ranking:</h3>
<p className="font-display italic text-6xl text-brand-red leading-none mt-1">#{position}</p>
</div>
</div>
</div>
{/* Right Column (History & Actions) */}
<div className="w-full lg:w-[55%] xl:w-[60%] flex flex-col gap-6 text-brand-dark-blue">
<div className="bg-brand-cream rounded-[2rem] p-6 sm:p-8 shadow-2xl flex-1 flex flex-col relative">
<h3 className="font-display text-3xl uppercase text-brand-blue mb-6">Ranking de Puntos</h3>
<div className="flex flex-col border border-gray-200 rounded-xl overflow-y-auto overflow-x-hidden bg-white/50 flex-1 max-h-[400px] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-gray-200 [&::-webkit-scrollbar-track]:rounded-full [&::-webkit-scrollbar-thumb]:bg-brand-blue [&::-webkit-scrollbar-thumb]:rounded-full">
{data.leaderboard.slice(0, 10).map((item, idx) => {
const isAprobada = true; // Use check icon for everyone
let rowBg = item.is_current_user ? 'bg-brand-yellow/20' : (idx % 2 === 0 ? 'bg-transparent' : 'bg-gray-50');
return (
<div key={idx} className={`flex items-center justify-between p-4 border-b border-gray-100 last:border-0 ${rowBg}`}>
<div className="flex items-center gap-2 w-1/3">
<span className="font-display text-brand-red text-xl ml-2 mr-2">#{idx + 1}</span>
<span className={`font-bold text-brand-navy`}>
{item.nombre}
</span>
</div>
<div className="text-brand-navy font-bold w-1/3 text-right">
{item.puntos} Pts
</div>
</div>
);
})}
</div>
</div>
<button
onClick={() => navigate('/upload')}
className="w-full bg-brand-yellow text-brand-navy font-bold text-xl py-5 rounded-full hover:bg-brand-yellow/90 active:scale-[0.98] transition-all shadow-lg shadow-brand-yellow/20"
>
Subir nueva factura
</button>
</div>
</div>
</div>
</div>
);
}
+133
View File
@@ -0,0 +1,133 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import step1Img from '../assets/images/step1-products.png';
import step2Img from '../assets/images/step2-user.png';
import step3Img from '../assets/images/step3-prizes.png';
import heroPeopleImg from '../assets/images/hero-people.png';
import heroMobileImg from '../assets/images/hero-mobile.png';
export default function Landing() {
const navigate = useNavigate();
const { user } = useAuth();
const handleActionClick = () => {
navigate(user ? '/upload' : '/auth');
};
return (
<div className="flex flex-col w-full bg-white font-sans text-brand-dark-blue">
{/* HERO SECTION */}
<section className="w-full relative bg-brand-blue">
<div className="w-full relative">
{/* Desktop Image */}
<img
src={heroPeopleImg}
alt="Cómete el Mundo y Gana"
className="hidden md:block w-full min-h-[400px] h-auto object-cover object-center"
/>
{/* Mobile Image */}
<img
src={heroMobileImg}
alt="Cómete el Mundo y Gana"
className="block md:hidden w-full h-auto object-cover object-center"
/>
{/* CTA Button placed over the image */}
<button
onClick={handleActionClick}
className="absolute bottom-[10%] md:bottom-[20%] left-[50%] -translate-x-1/2 md:translate-x-0 md:left-auto md:right-[15%] lg:right-[20%] z-20 bg-brand-yellow text-brand-blue font-bold px-8 md:px-10 py-3 md:py-4 rounded-full text-base md:text-lg shadow-[0_4px_20px_rgba(255,204,0,0.4)] hover:bg-white hover:text-brand-dark-blue hover:scale-105 transition-all"
>
Registra tu factura
</button>
</div>
</section>
{/* HOW TO PARTICIPATE */}
<section className="py-20 px-4 container mx-auto text-center">
<h2 className="font-display text-4xl text-brand-blue uppercase mb-12">¿Cómo participar?</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{/* Step 1 */}
<div className="rounded-xl overflow-hidden relative group hover:scale-[1.02] transition-transform shadow-lg">
<img src={step1Img} alt="Compra" className="w-full h-auto object-cover" />
</div>
{/* Step 2 */}
<div
onClick={handleActionClick}
className="rounded-xl overflow-hidden relative group hover:scale-[1.02] transition-transform shadow-lg cursor-pointer"
role="button"
tabIndex={0}
>
<img src={step2Img} alt="Regístrate" className="w-full h-auto object-cover" />
</div>
{/* Step 3 */}
<div className="rounded-xl overflow-hidden relative group hover:scale-[1.02] transition-transform shadow-lg">
<img src={step3Img} alt="Participa" className="w-full h-auto object-cover" />
</div>
</div>
<div className="mt-12 flex items-center justify-center gap-3 font-display uppercase text-2xl md:text-3xl">
<span className="text-brand-yellow"></span>
<span className="text-brand-blue">Recibe <span className="text-brand-red">1 punto</span> por cada <span className="text-brand-red">$ 10 USD</span></span>
<span className="text-brand-yellow"></span>
</div>
</section>
{/* MARCAS PARTICIPANTES */}
<section className="py-12 bg-white text-center">
<h2 className="font-display text-3xl text-brand-dark-blue uppercase mb-8">Marcas participantes</h2>
<div className="flex flex-col sm:flex-row justify-center items-center gap-8">
<div className="w-64 h-32 bg-brand-cream border border-gray-100 rounded-xl shadow-sm flex items-center justify-center hover:shadow-md transition-shadow p-6">
<img src={`${import.meta.env.BASE_URL}images/logo-nestle.png`} alt="Nestlé" className="max-w-full max-h-full object-contain" />
</div>
<div className="w-64 h-32 bg-brand-cream border border-gray-100 rounded-xl shadow-sm flex items-center justify-center hover:shadow-md transition-shadow p-6">
<img src={`${import.meta.env.BASE_URL}images/logo-maggi.png`} alt="Maggi" className="max-w-full max-h-full object-contain" />
</div>
</div>
</section>
{/* PRIZES HIGHLIGHT */}
<section className="py-16 px-4 max-w-6xl mx-auto text-center border-t border-gray-100">
<h2 className="font-display text-3xl md:text-4xl lg:text-5xl uppercase tracking-wide leading-tight mb-12">
<span className="text-brand-blue block sm:inline">Mientras más participes,</span>{" "}
<span className="text-brand-red block sm:inline">más oportunidades tienes de ganar.</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Loop fake prizes mimicking the grid (Asador, Utensilios, TV & Barra) */}
{[
{ name: 'Asador', img: `${import.meta.env.BASE_URL}images/premio-asador.png`, fallback: 'https://images.unsplash.com/photo-1555939594-58d7cb561ad1?q=80&w=1974&auto=format&fit=crop' },
{ name: 'Set de Parrillada', img: `${import.meta.env.BASE_URL}images/premio-utensilios.png`, fallback: 'https://images.unsplash.com/photo-1556910103-1c02745a873f?q=80&w=2070&auto=format&fit=crop' },
{ name: 'TV & Barra de Sonido', img: `${import.meta.env.BASE_URL}images/premio-tv.png`, fallback: 'https://images.unsplash.com/photo-1593359677879-a4bb92f829d1?q=80&w=2070&auto=format&fit=crop' },
].map((prize, i) => (
<div key={i} className="flex flex-col border border-gray-100 rounded-xl overflow-hidden hover:shadow-lg transition-shadow bg-white">
<div className="h-64 bg-gray-50 flex items-center justify-center relative border-b border-gray-100 overflow-hidden p-6">
<img
src={prize.img}
alt={prize.name}
className="w-full h-full object-contain transition-transform hover:scale-110 duration-300"
referrerPolicy="no-referrer"
onError={(e) => { e.currentTarget.src = prize.fallback }}
/>
</div>
<div className="p-4 text-center">
<p className="font-bold text-lg text-brand-dark-blue">{prize.name}</p>
</div>
</div>
))}
</div>
<button
onClick={handleActionClick}
className="mt-12 bg-brand-yellow text-brand-dark-blue font-bold px-12 py-4 rounded-full hover:bg-brand-yellow/80 transition-colors shadow-[0_4px_15px_rgba(255,204,0,0.4)] text-lg"
>
Registra tu factura
</button>
</section>
</div>
);
}
+182
View File
@@ -0,0 +1,182 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Camera, Upload, CheckCircle2, Loader2, FileImage } from 'lucide-react';
import { supabase } from '../lib/supabase';
import { useAuth } from '../contexts/AuthContext';
export default function UploadInvoice() {
const { user, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
// Redirect if not logged in
useEffect(() => {
if (authLoading) return;
if (!user && !loading && !success) {
navigate('/auth');
}
}, [user, authLoading, loading, success, navigate]);
if (authLoading || (!user && !loading && !success)) {
return null;
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
if (selectedFile.size > 5 * 1024 * 1024) {
setError('La imagen no debe superar los 5MB.');
return;
}
setFile(selectedFile);
setPreviewUrl(URL.createObjectURL(selectedFile));
setError(null);
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleSubmit = async () => {
if (!file || !user) return;
setLoading(true);
setError(null);
try {
const webhookUrl = import.meta.env.VITE_N8N_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error("La URL del webhook no está configurada.");
}
const formData = new FormData();
formData.append('file', file);
formData.append('user_id', user.id);
const response = await fetch(webhookUrl, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error("No pudimos procesar la factura. Intenta de nuevo.");
}
setSuccess(true);
} catch (err: any) {
console.error(err);
setError(err.message || "Ocurrió un error inesperado.");
} finally {
setLoading(false);
}
};
if (success) {
return (
<div className="flex-1 flex flex-col items-center justify-center p-6 animate-in zoom-in duration-300">
<div className="bg-white rounded-[3rem] p-8 text-center max-w-sm w-full shadow-2xl relative overflow-hidden">
<div className="absolute top-0 right-0 -mr-16 -mt-16 w-32 h-32 bg-brand-yellow/30 rounded-full blur-2xl"></div>
<div className="w-20 h-20 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 size={40} className="drop-shadow-sm" />
</div>
<h2 className="font-display text-4xl text-brand-navy uppercase mb-4 leading-none">¡Factura Enviada!</h2>
<p className="text-brand-navy/80 font-medium mb-8 leading-relaxed">
Tu ticket está en revisión por nuestro sistema. Pronto verás tus puntos reflejados.
</p>
<button
onClick={() => navigate('/dashboard')}
className="w-full bg-brand-navy text-white font-display text-xl uppercase py-4 rounded-2xl hover:bg-brand-navy/90 transition-all active:scale-95"
>
Ver mi progreso
</button>
</div>
</div>
);
}
return (
<div className="flex-1 p-4 pb-12 flex flex-col animate-in fade-in duration-300">
<div className="max-w-md w-full mx-auto mt-4">
<div className="text-center mb-8">
<h1 className="font-display text-4xl uppercase text-brand-yellow drop-shadow-md mb-2">
Registrar Factura
</h1>
<p className="text-white/90 font-medium">
Sube una foto clara de tu ticket
</p>
</div>
<div className="bg-white rounded-[2.5rem] p-6 shadow-xl text-brand-navy">
{error && (
<div className="bg-brand-red/10 text-brand-red p-4 rounded-2xl mb-6 text-sm border border-brand-red/20 text-center font-medium">
{error}
</div>
)}
<div
onClick={handleUploadClick}
className={`border-3 border-dashed rounded-3xl p-8 flex flex-col items-center justify-center text-center cursor-pointer transition-all ${
previewUrl ? 'border-brand-blue/30 bg-brand-blue/5' : 'border-black/10 bg-brand-cream hover:bg-brand-yellow/10 hover:border-brand-yellow'
}`}
>
{previewUrl ? (
<div className="relative w-full aspect-[3/4] max-h-64 rounded-2xl overflow-hidden shadow-inner">
<img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<p className="text-white font-bold inline-flex items-center gap-2">
<Camera size={20} /> Cambiar Foto
</p>
</div>
</div>
) : (
<div className="py-8 flex flex-col items-center">
<div className="w-16 h-16 bg-brand-yellow text-brand-navy rounded-full flex items-center justify-center mb-4 shadow-sm">
<Upload size={28} />
</div>
<h3 className="font-bold text-lg mb-1">Toca para subir</h3>
<p className="text-brand-navy/60 text-sm max-w-[200px]">Formatos: JPG, PNG. Máximo 5MB.</p>
</div>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/jpeg, image/png, image/webp"
className="hidden"
/>
</div>
<button
onClick={handleSubmit}
disabled={!file || loading}
className={`mt-6 w-full ${loading ? 'bg-brand-blue/80 opacity-100' : 'bg-brand-blue hover:bg-brand-blue/90'} ${!file && !loading ? 'opacity-50' : ''} text-white font-display text-lg uppercase py-4 rounded-2xl active:scale-[0.98] transition-all flex flex-col items-center justify-center gap-1 disabled:pointer-events-none shadow-lg shadow-brand-blue/20`}
>
{loading ? (
<>
<Loader2 className="animate-spin mb-1" size={24} />
<span className="text-sm normal-case font-medium">Analizando tu factura con Inteligencia Artificial... por favor espera</span>
</>
) : (
<span className="text-xl">Enviar Factura</span>
)}
</button>
</div>
</div>
</div>
);
}