Subir archivos a "src/pages"
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user