diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx new file mode 100644 index 0000000..2fc75bd --- /dev/null +++ b/src/pages/Auth.tsx @@ -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(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(null); + const recaptchaRef = useRef(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 ( +
+ {/* Dynamic Background */} +
+
+
+ {/* Abstract Texture Overlay */} +
+ Texture { 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 */ + /> +
+
+ +
+
+ + {/* White top right circle embellishment */} +
+ +

+ {isLogin ? 'LOGIN' : 'REGISTRO'} +

+ + {error && ( +
+ {error} +
+ )} + +
+ + {!isLogin && ( + <> +
+
+ + setNombre(e.target.value)} placeholder="Nombre" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ + setApellido(e.target.value)} placeholder="Apellido" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ +
+ + setEmail(e.target.value)} placeholder="correo@ejemplo.com" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+ +
+
+ + setPassword(e.target.value)} placeholder="Contraseña" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ + setConfirmPassword(e.target.value)} placeholder="Contraseña" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ +
+
+ + setAlias(e.target.value)} placeholder="Alias" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ + setCelular(e.target.value)} placeholder="+507 0000-0000" className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+
+ +
+ + setFechaNacimiento(e.target.value)} className="w-full bg-white text-gray-900 rounded-xl px-4 py-3 outline-none" /> +
+ +
+ setTermsAccepted(e.target.checked)} className="mt-1 w-4 h-4 rounded text-brand-blue" /> + +
+ + )} + + {isLogin && ( + <> +
+ + 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" /> +
+ +
+ + 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" /> + +
+ + )} + +
+ setCaptchaValue(value)} + theme="light" + /> +
+ + +
+ +
+ {isLogin ? ( + <>¿No tienes cuenta? + ) : ( + <>¿Ya tienes cuenta? + )} +
+ +
+
+
+ ); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..bb9c271 --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -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(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 ( +
+
+ +
+
+ ); + } + + 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 ( +
+ {/* Image Background */} +
+ Background +
+ +
+ + {/* Header Profile */} +
+
+ {initial} +
+

+ {fullName} +

+
+ +
+ {/* Left Column (Stats) */} +
+ + {/* Puntos Card */} +
+ {/* Star decorations */} +
+ +
+
+ +
+
+ +
+ +

Puntos Acumulados

+
+ {data.total_points} puntos +
+
+ + {/* Posicion Card */} +
+ +
+

Posición en ranking:

+

#{position}

+
+
+ +
+ + {/* Right Column (History & Actions) */} +
+ +
+

Ranking de Puntos

+ +
+ {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 ( +
+
+ #{idx + 1} + + {item.nombre} + +
+ +
+ {item.puntos} Pts +
+
+ ); + })} +
+
+ + + +
+
+ +
+
+ ); +} diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx new file mode 100644 index 0000000..8f12c88 --- /dev/null +++ b/src/pages/Landing.tsx @@ -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 ( +
+ + {/* HERO SECTION */} +
+
+ {/* Desktop Image */} + Cómete el Mundo y Gana + {/* Mobile Image */} + Cómete el Mundo y Gana + {/* CTA Button placed over the image */} + +
+
+ + {/* HOW TO PARTICIPATE */} +
+

¿Cómo participar?

+ +
+ {/* Step 1 */} +
+ Compra +
+ + {/* Step 2 */} +
+ Regístrate +
+ + {/* Step 3 */} +
+ Participa +
+
+ +
+ + Recibe 1 punto por cada $ 10 USD + +
+
+ + {/* MARCAS PARTICIPANTES */} +
+

Marcas participantes

+
+
+ Nestlé +
+
+ Maggi +
+
+
+ + {/* PRIZES HIGHLIGHT */} +
+

+ Mientras más participes,{" "} + más oportunidades tienes de ganar. +

+ +
+ {/* 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) => ( +
+
+ {prize.name} { e.currentTarget.src = prize.fallback }} + /> +
+
+

{prize.name}

+
+
+ ))} +
+ + +
+ +
+ ); +} diff --git a/src/pages/UploadInvoice.tsx b/src/pages/UploadInvoice.tsx new file mode 100644 index 0000000..48fb83f --- /dev/null +++ b/src/pages/UploadInvoice.tsx @@ -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(null); + + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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) => { + 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 ( +
+
+
+ +
+ +
+ +

¡Factura Enviada!

+ +

+ Tu ticket está en revisión por nuestro sistema. Pronto verás tus puntos reflejados. +

+ + +
+
+ ); + } + + return ( +
+
+ +
+

+ Registrar Factura +

+

+ Sube una foto clara de tu ticket +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ {previewUrl ? ( +
+ Preview +
+

+ Cambiar Foto +

+
+
+ ) : ( +
+
+ +
+

Toca para subir

+

Formatos: JPG, PNG. Máximo 5MB.

+
+ )} + +
+ + + +
+
+
+ ); +}