Merge branch 'feature/style-fix' into 'main'

Added demo visualization in Login and Register

See merge request fjmimbre/deck-of-cards!2
This commit is contained in:
alexis
2026-05-29 09:20:43 +00:00
3 changed files with 532 additions and 85 deletions
+407
View File
@@ -0,0 +1,407 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
ComposedChart, Area, Line,
XAxis, YAxis, CartesianGrid,
ReferenceArea, ReferenceLine,
ResponsiveContainer,
} from 'recharts';
// ── Fake demo data ──────────────────────────────────────────────────────────
const STEP1_CARDS = ['Bajo', 'Medio', 'Alto', 'Perfecto'];
const STEP1_BLANKS = [3, 1, 4]; // huecos asimétricos entre cartas
const STEP2_TERMS = [
{ name: 'Bajo', xVal: 0.08, mf: { supportStart: 0.00, coreStart: 0.00, coreEnd: 0.06, supportEnd: 0.30 } },
{ name: 'Medio', xVal: 0.38, mf: { supportStart: 0.18, coreStart: 0.38, coreEnd: 0.38, supportEnd: 0.59 } }, // pico/triángulo
{ name: 'Alto', xVal: 0.65, mf: { supportStart: 0.52, coreStart: 0.60, coreEnd: 0.69, supportEnd: 0.77 } },
{ name: 'Perfecto', xVal: 0.92, mf: { supportStart: 0.74, coreStart: 0.88, coreEnd: 1.00, supportEnd: 1.00 } },
];
const STEP2_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6'];
// Igual que interpolateY en useGraphData.js:
// · Zona buffer justo fuera del soporte → 0 (ancla la línea en y=0)
// · Más allá del buffer → null (corte, sin línea horizontal)
const TRAP_BUF = 1.1e-4;
function trapVal(x, s0, c0, c1, s1) {
if (x < s0 - TRAP_BUF || x > s1 + TRAP_BUF) return null;
if (x < s0 || x > s1) return 0;
if (x >= c0 && x <= c1) return 1;
if (x < c0 && c0 > s0) return (x - s0) / (c0 - s0);
if (x > c1 && s1 > c1) return (s1 - x) / (s1 - c1);
return 0;
}
// Solo 'Alto' es IT2 (tiene subescala). UMF más ancha; LMF con el mismo núcleo trapecial que el paso 2.
const STEP3_TERMS = [
{ name: 'Bajo', color: '#ef4444', type: 't1', pts: [0.00, 0.00, 0.06, 0.30] },
{ name: 'Medio', color: '#f59e0b', type: 't1', pts: [0.18, 0.38, 0.38, 0.59] },
{ name: 'Alto', color: '#10b981', type: 't2', u: [0.49, 0.57, 0.71, 0.81], l: [0.56, 0.60, 0.69, 0.76] },
{ name: 'Perfecto', color: '#3b82f6', type: 't1', pts: [0.74, 0.88, 1.00, 1.00] },
];
// Puntos clave del trapecio (piezas lineales → basta con vértices, como en el paso 2).
// Recharts interpola el trazo entre ellos con animación fluida.
function getTermLineData(term) {
const name = term.name;
if (term.type === 't1') {
const [s0, c0, c1, s1] = term.pts;
const xs = new Set([s0, c0, c1, s1]);
if (s0 <= 0.001) xs.add(s0 - TRAP_BUF);
if (s1 >= 0.999) xs.add(s1 + TRAP_BUF);
return Array.from(xs).sort((a, b) => a - b).map(x => ({
x,
[name]: trapVal(x, s0, c0, c1, s1),
}));
}
const xs = new Set([...term.u, ...term.l]);
return Array.from(xs).sort((a, b) => a - b).map(x => {
const upper = trapVal(x, ...term.u);
const lower = trapVal(x, ...term.l);
return {
x,
[`${name}_upper`]: upper,
[`${name}_lower`]: lower,
[`${name}_range`]: (lower === null && upper === null)
? null
: [lower ?? 0, upper ?? 0],
};
});
}
const STEP_LABELS = [
{ n: 1, label: 'Escala' },
{ n: 2, label: 'Modelado' },
{ n: 3, label: 'Espectro IT2' },
];
// ── Step sub-components ─────────────────────────────────────────────────────
function Step1Content({ count }) {
const done = count >= STEP1_CARDS.length;
return (
<div className="w-full flex flex-col items-center justify-center gap-4 py-2">
<p className="text-[11px] font-bold text-blue-500 self-start">Criterio: Calidad Investigadora</p>
<div className="flex items-center justify-center">
{STEP1_CARDS.map((name, i) => (
<React.Fragment key={name}>
{i > 0 && (
<div className="flex flex-col items-center mx-1 mb-9">
<div className={`h-px w-10 transition-all duration-500 ${i < count ? 'bg-slate-300' : 'bg-slate-100'}`} />
<span className={`text-[10px] font-bold mt-1 transition-all duration-500 ${i < count ? 'text-slate-400' : 'text-slate-100'}`}>
×{STEP1_BLANKS[i - 1]}
</span>
</div>
)}
<div
style={{
opacity: i < count ? 1 : 0,
transform: i < count ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.85)',
transition: 'opacity 0.4s ease, transform 0.4s ease',
}}
>
<div className={`w-20 h-28 bg-white border-2 rounded-2xl shadow-sm flex flex-col items-center justify-center relative transition-all duration-300 ${i === count - 1 ? 'border-blue-300 shadow-blue-100 shadow-md' : 'border-slate-200'}`}>
<span className="absolute top-1.5 left-2.5 text-[10px] font-black text-slate-200">{i + 1}</span>
<span className="absolute bottom-1.5 right-2.5 text-[10px] font-black text-slate-200 rotate-180">{i + 1}</span>
<span className="text-xs font-bold text-slate-700 text-center px-1.5 leading-tight">{name}</span>
</div>
</div>
</React.Fragment>
))}
</div>
<div className={`flex items-center justify-center gap-2.5 transition-all duration-500 ${done ? 'opacity-100' : 'opacity-0'}`}>
<span className="text-xs font-bold text-emerald-600"> {STEP1_CARDS.length} niveles definidos</span>
<div className="h-1.5 w-24 rounded-full bg-emerald-100">
<div className="h-1.5 rounded-full bg-emerald-500 w-full" />
</div>
</div>
</div>
);
}
function Step2Content({ count }) {
const visibleTerms = STEP2_TERMS.slice(0, count);
const activeIndex = count - 1;
const showSubscale = count >= 3;
return (
<div className="w-full flex flex-col justify-center gap-2 py-1">
<div className="flex flex-wrap gap-1.5">
{STEP2_TERMS.map((term, i) => {
const color = STEP2_COLORS[i % STEP2_COLORS.length];
const isVisible = i < count;
const isActive = i === activeIndex;
return (
<span
key={term.name}
className="px-2.5 py-0.5 rounded-lg text-[10px] font-bold border-2 transition-all duration-500"
style={
isActive
? { backgroundColor: color, borderColor: color, color: '#fff', transform: 'scale(1.1)' }
: isVisible
? { borderColor: color, color: '#64748b', backgroundColor: 'white' }
: { borderColor: '#e2e8f0', color: '#cbd5e1', backgroundColor: 'white' }
}
>
{term.name}
{i === 2 && showSubscale && (
<span className="ml-1 text-[8px] font-black bg-purple-100 text-purple-600 rounded px-0.5">IT2</span>
)}
</span>
);
})}
</div>
<div className="w-full rounded-2xl border border-slate-200 p-2" style={{ backgroundColor: '#f8fafc' }}>
<ResponsiveContainer width="99%" height={148}>
<ComposedChart margin={{ top: 12, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis type="number" dataKey="x" domain={[0, 1]} ticks={[0, 0.25, 0.5, 0.75, 1]} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 600 }} />
<YAxis domain={[0, 1]} ticks={[0, 0.5, 1]} tick={{ fill: '#94a3b8', fontSize: 10 }} width={24} />
{visibleTerms.map((term, i) => {
const color = STEP2_COLORS[i % STEP2_COLORS.length];
const isActive = i === activeIndex;
return (
<ReferenceLine
key={`ref-${term.name}`}
x={term.xVal}
stroke={color}
strokeDasharray="4 4"
strokeWidth={isActive ? 2 : 1}
label={{ position: 'top', value: term.name, fill: color, fontWeight: isActive ? '900' : '600', fontSize: 10 }}
/>
);
})}
{visibleTerms.map((term, i) => {
const color = STEP2_COLORS[i % STEP2_COLORS.length];
const isActive = i === activeIndex;
return (
<ReferenceArea
key={`area-${term.name}`}
x1={term.mf.supportStart}
x2={term.mf.supportEnd}
fill={color}
fillOpacity={isActive ? 0.22 : 0.07}
/>
);
})}
{visibleTerms.map((term, i) => {
const color = STEP2_COLORS[i % STEP2_COLORS.length];
const isActive = i === activeIndex;
const trapezeData = [
{ x: term.mf.supportStart, y: 0 },
{ x: term.mf.coreStart, y: 1 },
{ x: term.mf.coreEnd, y: 1 },
{ x: term.mf.supportEnd, y: 0 },
];
return (
<Line
key={`line-${term.name}`}
data={trapezeData}
dataKey="y"
type="linear"
stroke={color}
strokeWidth={isActive ? 3 : 2}
dot={isActive ? { r: 4, fill: color, stroke: '#fff', strokeWidth: 2 } : false}
activeDot={false}
isAnimationActive={isActive}
animationDuration={500}
animationEasing="ease-out"
/>
);
})}
</ComposedChart>
</ResponsiveContainer>
</div>
{/* Mini SubscaleModal inline */}
<div className={`w-full rounded-xl border border-purple-200 bg-purple-50/60 overflow-hidden transition-all duration-500 ${showSubscale ? 'opacity-100 max-h-32' : 'opacity-0 max-h-0 border-transparent pointer-events-none'}`}>
<div className="px-3 py-1.5 border-b border-purple-100 flex items-center gap-1.5">
<span className="text-[9px] font-black text-purple-700 uppercase tracking-wider">Diseñar Subescala</span>
<span className="text-[9px] text-purple-300">·</span>
<span className="text-[9px] font-bold text-emerald-600">Alto</span>
<span className="text-[9px] text-purple-300">·</span>
<span className="text-[9px] text-slate-400">Pendiente Descendente</span>
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-purple-400 animate-pulse" />
</div>
<div className="px-3 py-2.5 flex items-center justify-center gap-2">
<div className="w-9 h-12 bg-white border-2 border-slate-200 rounded-lg flex items-center justify-center shrink-0 shadow-sm">
<span className="text-xs font-black text-slate-300">1</span>
</div>
<div className="flex flex-col items-center gap-0.5">
<div className="flex items-end gap-1">
<div className="flex flex-col items-center">
<span className="text-[7px] font-bold text-slate-400 leading-none">MÍN</span>
<span className="text-[10px] font-black text-slate-700 bg-white border border-slate-200 rounded px-1.5 py-0.5 shadow-sm">2</span>
</div>
<span className="text-[9px] text-slate-300 mb-0.5"></span>
<div className="flex flex-col items-center">
<span className="text-[7px] font-bold text-slate-400 leading-none">MÁX</span>
<span className="text-[10px] font-black text-slate-700 bg-white border border-slate-200 rounded px-1.5 py-0.5 shadow-sm">5</span>
</div>
</div>
<span className="text-[8px] font-bold text-blue-500 whitespace-nowrap">¿Dudas? Rango </span>
</div>
<div className="w-9 h-12 bg-white border-2 border-slate-200 rounded-lg flex items-center justify-center shrink-0 shadow-sm">
<span className="text-xs font-black text-slate-300">2</span>
</div>
<div className="flex flex-col items-center gap-0.5">
<div className="flex flex-col items-center">
<span className="text-[7px] font-bold text-slate-400 leading-none">CARTAS</span>
<span className="text-[10px] font-black text-slate-700 bg-white border border-slate-200 rounded px-1.5 py-0.5 shadow-sm">3</span>
</div>
<span className="text-[8px] font-semibold text-slate-400 whitespace-nowrap">Distancia exacta</span>
</div>
<div className="w-9 h-12 bg-white border-2 border-slate-200 rounded-lg flex items-center justify-center shrink-0 shadow-sm">
<span className="text-xs font-black text-slate-300">3</span>
</div>
</div>
</div>
</div>
);
}
function Step3Content({ count }) {
return (
<div className="w-full flex flex-col items-center justify-center gap-2 py-1">
<p className="text-[11px] font-bold text-blue-500 self-start">Espectro difuso · Calidad Investigadora</p>
<div className="w-full rounded-2xl border border-slate-200 px-2 pt-2 pb-1.5" style={{ backgroundColor: '#f8fafc' }}>
<ResponsiveContainer width="99%" height={168}>
<ComposedChart margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis type="number" dataKey="x" domain={[0, 1]} allowDataOverflow={true} ticks={[0, 0.25, 0.5, 0.75, 1]} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 600 }} />
<YAxis domain={[0, 1]} ticks={[0, 0.5, 1]} tick={{ fill: '#94a3b8', fontSize: 10 }} width={24} />
{STEP3_TERMS.slice(0, count).map((term, i) => {
const isNewest = i === count - 1;
const data = getTermLineData(term);
return (
<React.Fragment key={term.name}>
{term.type === 't1' ? (
<Line
data={data}
type="linear"
dataKey={term.name}
stroke={term.color}
strokeWidth={2.5}
dot={false}
connectNulls={false}
isAnimationActive={isNewest}
animationDuration={900}
animationEasing="ease-out"
/>
) : (
<>
<Area data={data} type="linear" dataKey={`${term.name}_range`} fill={term.color} fillOpacity={0.35} stroke="none" connectNulls={false} isAnimationActive={isNewest} animationDuration={900} animationEasing="ease-out" />
<Line data={data} type="linear" dataKey={`${term.name}_upper`} stroke={term.color} strokeWidth={1.5} strokeDasharray="5 4" dot={false} connectNulls={false} isAnimationActive={isNewest} animationDuration={900} animationEasing="ease-out" />
<Line data={data} type="linear" dataKey={`${term.name}_lower`} stroke={term.color} strokeWidth={2.5} dot={false} connectNulls={false} isAnimationActive={isNewest} animationDuration={900} animationEasing="ease-out" />
</>
)}
</React.Fragment>
);
})}
</ComposedChart>
</ResponsiveContainer>
<div className="flex flex-wrap justify-center gap-x-3 gap-y-0.5 px-1 pt-0.5 pb-0.5">
{STEP3_TERMS.map((term, i) => (
<div
key={term.name}
className="flex items-center gap-1.5 transition-all duration-500"
style={{ opacity: i < count ? 1 : 0, transform: i < count ? 'translateY(0)' : 'translateY(4px)' }}
>
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: term.color }} />
<span className="text-[10px] font-bold" style={{ color: term.color }}>{term.name}</span>
{term.type === 't2' && i < count && (
<span className="text-[8px] font-black bg-purple-100 text-purple-600 rounded px-0.5">IT2</span>
)}
</div>
))}
</div>
</div>
</div>
);
}
// ── Main component ───────────────────────────────────────────────────────────
export default function AuthDemoPanel() {
const [step, setStep] = useState(1);
const [count, setCount] = useState(0);
const [fading, setFading] = useState(false);
const innerTimerRef = useRef(null);
const transitionTo = useCallback((nextStep) => {
if (innerTimerRef.current) clearTimeout(innerTimerRef.current);
setFading(true);
innerTimerRef.current = setTimeout(() => {
setStep(nextStep);
setCount(0);
setFading(false);
}, 440);
}, []);
useEffect(() => {
let timeout;
if (step === 1) {
if (count < STEP1_CARDS.length) {
timeout = setTimeout(() => setCount(c => c + 1), 580);
} else {
timeout = setTimeout(() => transitionTo(2), 950);
}
} else if (step === 2) {
if (count < STEP2_TERMS.length) {
timeout = setTimeout(() => setCount(c => c + 1), 920);
} else {
timeout = setTimeout(() => transitionTo(3), 1300);
}
} else if (step === 3) {
if (count < STEP3_TERMS.length) {
timeout = setTimeout(() => setCount(c => c + 1), 920);
} else {
timeout = setTimeout(() => transitionTo(1), 2800);
}
}
return () => clearTimeout(timeout);
}, [step, count, transitionTo]);
useEffect(() => {
return () => { if (innerTimerRef.current) clearTimeout(innerTimerRef.current); };
}, []);
return (
<div
className="mt-5 flex-1 w-full flex flex-col gap-3 min-h-0 transition-opacity duration-500"
style={{ opacity: fading ? 0 : 1 }}
>
{/* Step breadcrumb */}
<div className="shrink-0 flex items-center gap-1">
{STEP_LABELS.map((s, i) => (
<React.Fragment key={s.n}>
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-lg transition-all duration-300 ${step === s.n ? 'bg-blue-100' : ''}`}>
<span className={`w-4 h-4 rounded-full text-[9px] flex items-center justify-center font-black transition-all duration-300 ${step === s.n ? 'bg-blue-600 text-white' : step > s.n ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-400'}`}>
{step > s.n ? '✓' : s.n}
</span>
<span className={`text-[10px] font-bold transition-all duration-300 ${step === s.n ? 'text-blue-700' : step > s.n ? 'text-emerald-600' : 'text-slate-400'}`}>
{s.label}
</span>
</div>
{i < STEP_LABELS.length - 1 && (
<span className="text-slate-300 text-xs mx-0.5"></span>
)}
</React.Fragment>
))}
<span className="ml-auto flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-[10px] text-emerald-500 font-bold">En vivo</span>
</span>
</div>
<div className="flex-1 flex flex-col justify-center min-h-0">
{step === 1 && <Step1Content count={count} />}
{step === 2 && <Step2Content count={count} />}
{step === 3 && <Step3Content count={count} />}
</div>
</div>
);
}
+74 -54
View File
@@ -3,7 +3,8 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { authService } from '../services/authService'; import { authService } from '../services/authService';
import { API_BASE_URL } from '../config'; import { API_BASE_URL } from '../config';
import { FiEye, FiEyeOff } from 'react-icons/fi'; import { FiArrowLeft, FiEye, FiEyeOff } from 'react-icons/fi';
import AuthDemoPanel from '../components/AuthDemoPanel';
export default function Login() { export default function Login() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -71,70 +72,89 @@ export default function Login() {
}; };
return ( return (
<div className="flex-1 flex items-center justify-center py-4"> <div className="w-full flex items-start justify-center">
<div className="max-w-md w-full bg-white p-10 rounded-3xl shadow-sm border border-slate-200"> <div className="w-full grid gap-6 lg:gap-8 lg:grid-cols-[minmax(0,1fr)_26rem]">
<div className="text-center mb-8"> <div className="hidden lg:flex flex-col justify-start rounded-3xl border border-blue-100 bg-linear-to-br from-blue-50 via-indigo-50 to-sky-50 p-10 self-stretch min-h-0">
<h2 className="text-3xl font-black text-slate-800 tracking-tight">Deck of Cards</h2> <p className="text-xs font-black uppercase tracking-[0.2em] text-blue-500">Deck of Cards</p>
<p className="text-slate-500 mt-2">Accede a tu historial y gráficas guardadas</p> <h1 className="mt-4 text-4xl font-black tracking-tight text-slate-800">Modela y compara de forma visual</h1>
<p className="mt-3 text-slate-500 text-sm leading-relaxed">
Construye funciones de pertenencia difusa, guarda tu historial y vuelve a trabajar donde lo dejaste.
</p>
<Link
to="/editor"
className="mt-6 inline-flex w-fit items-center rounded-xl bg-slate-900 px-5 py-3 text-sm font-bold text-white transition-colors hover:bg-slate-800"
>
<FiArrowLeft className="mr-2 h-4 w-4" />
Ir al editor principal
</Link>
<AuthDemoPanel />
</div> </div>
{error && ( <div className="w-full bg-white p-8 sm:p-10 rounded-3xl shadow-sm border border-slate-200">
<div className="bg-red-50 text-red-600 p-4 rounded-2xl text-sm font-bold mb-6 border border-red-100 text-center"> <div className="text-center mb-8">
{error} <h2 className="text-3xl font-black text-slate-800 tracking-tight">Deck of Cards</h2>
<p className="text-slate-500 mt-2">Accede a tu historial y gráficas guardadas</p>
</div> </div>
)}
<form onSubmit={handleSubmit} className="space-y-4"> {error && (
<div className="space-y-1"> <div className="bg-red-50 text-red-600 p-4 rounded-2xl text-sm font-bold mb-6 border border-red-100 text-center">
<label className="text-sm font-bold text-slate-700 ml-1">Email</label> {error}
<input
type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full px-5 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white"
placeholder="correo@ejemplo.com"
/>
</div>
<div className="space-y-1">
<label className="text-sm font-bold text-slate-700 ml-1">Contraseña</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
required value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full pl-5 pr-12 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors focus:outline-none"
>
{showPassword ? (
<FiEye className="w-5 h-5" strokeWidth={2} />
) : (
<FiEyeOff className="w-5 h-5" strokeWidth={2} />
)}
</button>
</div> </div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<label className="text-sm font-bold text-slate-700 ml-1">Email</label>
<input
type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full px-5 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white"
placeholder="correo@ejemplo.com"
/>
</div>
<div className="space-y-1">
<label className="text-sm font-bold text-slate-700 ml-1">Contraseña</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
required value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full pl-5 pr-12 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors focus:outline-none"
>
{showPassword ? (
<FiEye className="w-5 h-5" strokeWidth={2} />
) : (
<FiEyeOff className="w-5 h-5" strokeWidth={2} />
)}
</button>
</div>
</div>
<button type="submit" className="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-2xl transition-all shadow-sm active:scale-95 mt-2">
Entrar
</button>
</form>
<div className="relative my-8">
<div className="absolute inset-0 flex items-center"><div className="w-full border-t border-slate-100"></div></div>
<div className="relative flex justify-center text-xs uppercase tracking-widest"><span className="px-3 bg-white text-slate-400 font-bold">O</span></div>
</div> </div>
<button type="submit" className="w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-2xl transition-all shadow-sm active:scale-95 mt-2"> <button type="button" onClick={handleGoogleLogin} className="w-full flex items-center justify-center gap-3 px-4 py-4 border-2 border-slate-100 rounded-2xl bg-white text-slate-700 font-bold hover:bg-slate-50 hover:border-slate-200 transition-all shadow-sm active:scale-95">
Entrar <svg className="w-5 h-5" viewBox="0 0 24 24"><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="#4285F4" /><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="#34A853" /><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="#FBBC05" /><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="#EA4335" /></svg>
Continuar con Google
</button> </button>
</form>
<div className="relative my-8"> <p className="mt-8 text-center text-sm text-slate-500 font-medium">¿Nuevo por aquí? <Link to="/register" className="text-blue-600 hover:underline font-extrabold">Crea una cuenta</Link></p>
<div className="absolute inset-0 flex items-center"><div className="w-full border-t border-slate-100"></div></div>
<div className="relative flex justify-center text-xs uppercase tracking-widest"><span className="px-3 bg-white text-slate-400 font-bold">O</span></div>
</div> </div>
<button type="button" onClick={handleGoogleLogin} className="w-full flex items-center justify-center gap-3 px-4 py-4 border-2 border-slate-100 rounded-2xl bg-white text-slate-700 font-bold hover:bg-slate-50 hover:border-slate-200 transition-all shadow-sm active:scale-95">
<svg className="w-5 h-5" viewBox="0 0 24 24"><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="#4285F4" /><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="#34A853" /><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="#FBBC05" /><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="#EA4335" /></svg>
Continuar con Google
</button>
<p className="mt-8 text-center text-sm text-slate-500 font-medium">¿Nuevo por aquí? <Link to="/register" className="text-blue-600 hover:underline font-extrabold">Crea una cuenta</Link></p>
</div> </div>
</div> </div>
); );
} }
+51 -31
View File
@@ -2,7 +2,8 @@ import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { authService } from '../services/authService'; import { authService } from '../services/authService';
import { FiEye, FiEyeOff } from 'react-icons/fi'; import { FiArrowLeft, FiEye, FiEyeOff } from 'react-icons/fi';
import AuthDemoPanel from '../components/AuthDemoPanel';
export default function Register() { export default function Register() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@@ -95,34 +96,52 @@ export default function Register() {
}; };
return ( return (
<div className="flex-1 flex items-center justify-center py-4"> <div className="w-full flex items-start justify-center">
<div className="max-w-md w-full bg-white p-10 rounded-3xl shadow-sm border border-slate-200"> <div className="w-full grid gap-6 lg:gap-8 lg:grid-cols-[minmax(0,1fr)_26rem]">
<div className="hidden lg:flex flex-col justify-start rounded-3xl border border-indigo-100 bg-linear-to-br from-indigo-50 via-violet-50 to-blue-50 p-10 self-stretch min-h-0">
<div className="text-center mb-8"> <p className="text-xs font-black uppercase tracking-[0.2em] text-indigo-500">Deck of Cards</p>
<h2 className="text-3xl font-black text-slate-800 tracking-tight"> <h1 className="mt-4 text-4xl font-black tracking-tight text-slate-800">
{verificationRequired ? 'Verifica tu email' : 'Crear Cuenta'} Crea tu cuenta y guarda cada modelo
</h2> </h1>
<p className="text-slate-500 mt-2"> <p className="mt-3 text-slate-500 text-sm leading-relaxed">
{verificationRequired Registra tus criterios, conserva resultados en el historial y retoma tus análisis cuando quieras.
? `Introduce el código enviado a ${pendingEmail}`
: 'Inicia sesión para guardar tu progreso'}
</p> </p>
<Link
to="/editor"
className="mt-6 inline-flex w-fit items-center rounded-xl bg-slate-900 px-5 py-3 text-sm font-bold text-white transition-colors hover:bg-slate-800"
>
<FiArrowLeft className="mr-2 h-4 w-4" />
Ir al editor principal
</Link>
<AuthDemoPanel />
</div> </div>
{error && ( <div className="w-full bg-white p-8 sm:p-10 rounded-3xl shadow-sm border border-slate-200">
<div className="bg-red-50 text-red-600 p-4 rounded-2xl text-sm font-bold mb-6 border border-red-100 text-center"> <div className="text-center mb-8">
{error} <h2 className="text-3xl font-black text-slate-800 tracking-tight">
{verificationRequired ? 'Verifica tu email' : 'Crear Cuenta'}
</h2>
<p className="text-slate-500 mt-2">
{verificationRequired
? `Introduce el código enviado a ${pendingEmail}`
: 'Inicia sesión para guardar tu progreso'}
</p>
</div> </div>
)}
{infoMessage && ( {error && (
<div className="bg-blue-50 text-blue-700 p-4 rounded-2xl text-sm font-bold mb-6 border border-blue-100 text-center"> <div className="bg-red-50 text-red-600 p-4 rounded-2xl text-sm font-bold mb-6 border border-red-100 text-center">
{infoMessage} {error}
</div> </div>
)} )}
{!verificationRequired ? ( {infoMessage && (
<form onSubmit={handleRegisterSubmit} className="space-y-4"> <div className="bg-blue-50 text-blue-700 p-4 rounded-2xl text-sm font-bold mb-6 border border-blue-100 text-center">
{infoMessage}
</div>
)}
{!verificationRequired ? (
<form onSubmit={handleRegisterSubmit} className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-bold text-slate-700 ml-1">Nombre de usuario</label> <label className="text-sm font-bold text-slate-700 ml-1">Nombre de usuario</label>
<input <input
@@ -196,9 +215,9 @@ export default function Register() {
> >
{isSubmitting ? 'Enviando código...' : 'Registrarse'} {isSubmitting ? 'Enviando código...' : 'Registrarse'}
</button> </button>
</form> </form>
) : ( ) : (
<form onSubmit={handleVerificationSubmit} className="space-y-4"> <form onSubmit={handleVerificationSubmit} className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-bold text-slate-700 ml-1">Código de verificación</label> <label className="text-sm font-bold text-slate-700 ml-1">Código de verificación</label>
<input <input
@@ -230,12 +249,13 @@ export default function Register() {
> >
{isResending ? 'Reenviando...' : 'Reenviar código'} {isResending ? 'Reenviando...' : 'Reenviar código'}
</button> </button>
</form> </form>
)} )}
<p className="mt-8 text-center text-sm text-slate-500 font-medium"> <p className="mt-8 text-center text-sm text-slate-500 font-medium">
¿Ya tienes cuenta? <Link to="/login" className="text-blue-600 hover:underline font-extrabold">Inicia sesión aquí</Link> ¿Ya tienes cuenta? <Link to="/login" className="text-blue-600 hover:underline font-extrabold">Inicia sesión aquí</Link>
</p> </p>
</div>
</div> </div>
</div> </div>
); );