add: implementar login/registro
This commit is contained in:
@@ -1,26 +1,55 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
|
const { user, isAuthenticated, logout } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
<div className="min-h-screen bg-slate-50 flex flex-col">
|
||||||
|
|
||||||
{/* Cabecera */}
|
<header className="bg-white border-b border-slate-200 sticky top-0 z-50">
|
||||||
<header className="bg-white border-b border-slate-200 sticky top-0 z-50 shadow-sm">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||||
<div className="max-w-7xl mx-auto px-4 h-14 flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-inner">
|
<Link to="/" className="flex items-center gap-3 group">
|
||||||
<span className="text-white font-black text-xl leading-none">DoC</span>
|
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-sm group-hover:bg-blue-700 transition-colors">
|
||||||
|
DoC
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold text-slate-800 tracking-tight group-hover:text-blue-600 transition-colors">
|
||||||
|
Deck of Cards
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 bg-indigo-100 text-indigo-700 rounded-full flex items-center justify-center font-bold text-lg shadow-sm border border-indigo-200 cursor-pointer hover:bg-indigo-200 transition-colors"
|
||||||
|
title={`Cerrar sesión de ${user?.username}`}
|
||||||
|
onClick={() => {
|
||||||
|
if(window.confirm('¿Deseas cerrar sesión?')) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user?.username?.charAt(0).toUpperCase() || 'U'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="px-5 py-2 bg-blue-50 text-blue-600 font-bold rounded-lg hover:bg-blue-100 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Iniciar Sesión
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-bold text-slate-800">
|
|
||||||
Deck of Cards
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Contenido principal */}
|
<main className="flex-1 w-full max-w-7xl mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
<main className="max-w-7xl mx-auto px-4 py-6">
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,12 @@ const api = Axios.create({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import CriterionInput from '../components/CriterionInput';
|
|
||||||
import CardEditor from '../components/CardEditor';
|
|
||||||
import BlankCardsCounter from '../components/BlankCardsCounter';
|
|
||||||
import AddLevelButton from '../components/AddLevelButton';
|
|
||||||
import Chart from '../components/membershipFunction/Chart';
|
|
||||||
import Controls from '../components/membershipFunction/Controls';
|
|
||||||
import { calculateValueFunction } from '../services/docService';
|
|
||||||
|
|
||||||
const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1'];
|
|
||||||
|
|
||||||
export default function AdvancedMode() {
|
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const [criterionName, setCriterionName] = useState('');
|
|
||||||
const [levels, setLevels] = useState(['', '', '']);
|
|
||||||
const [blankCards, setBlankCards] = useState([0, 0]);
|
|
||||||
const [errors, setErrors] = useState({ criterion: false, levels: [] });
|
|
||||||
|
|
||||||
const [isZoomActive, setIsZoomActive] = useState(true);
|
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const tableRef = useRef(null);
|
|
||||||
const [dimensions, setDimensions] = useState({ container: 1000, table: 0 });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateMeasurements = () => {
|
|
||||||
if (containerRef.current && tableRef.current) {
|
|
||||||
setDimensions({
|
|
||||||
container: containerRef.current.offsetWidth,
|
|
||||||
table: tableRef.current.scrollWidth
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const timeoutId = setTimeout(updateMeasurements, 50);
|
|
||||||
window.addEventListener('resize', updateMeasurements);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
window.removeEventListener('resize', updateMeasurements);
|
|
||||||
};
|
|
||||||
}, [levels, blankCards, step]);
|
|
||||||
|
|
||||||
// Estados Fase 2 (Franjas)
|
|
||||||
const [baseScale, setBaseScale] = useState({});
|
|
||||||
const [selectedTerm, setSelectedTerm] = useState(null);
|
|
||||||
const [mfDefinitions, setMfDefinitions] = useState({});
|
|
||||||
|
|
||||||
// Manejadores de Escala
|
|
||||||
const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); };
|
|
||||||
const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); };
|
|
||||||
const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); };
|
|
||||||
const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); };
|
|
||||||
const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } };
|
|
||||||
|
|
||||||
const handleGenerateBaseScale = async () => {
|
|
||||||
const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) };
|
|
||||||
if (newErrors.criterion || newErrors.levels.includes(true)) {
|
|
||||||
setErrors(newErrors);
|
|
||||||
return alert("Por favor, rellena todos los campos.");
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } };
|
|
||||||
const baseResult = await calculateValueFunction(payloadBase);
|
|
||||||
|
|
||||||
setBaseScale(baseResult.values);
|
|
||||||
const initialMfs = {};
|
|
||||||
Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; });
|
|
||||||
|
|
||||||
setMfDefinitions(initialMfs);
|
|
||||||
setSelectedTerm(Object.keys(baseResult.values)[0]);
|
|
||||||
setStep(2);
|
|
||||||
} catch (error) { alert("Error: " + error); } finally { setIsLoading(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCurrentMf = (field, value) => {
|
|
||||||
if (!selectedTerm) return;
|
|
||||||
let numValue = parseFloat(value);
|
|
||||||
|
|
||||||
setMfDefinitions(prev => {
|
|
||||||
const scaleKeys = Object.keys(baseScale);
|
|
||||||
const selectedIndex = scaleKeys.indexOf(selectedTerm);
|
|
||||||
let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1;
|
|
||||||
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd;
|
|
||||||
prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd;
|
|
||||||
}
|
|
||||||
if (selectedIndex < scaleKeys.length - 1) {
|
|
||||||
nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart;
|
|
||||||
nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchor = baseScale[selectedTerm];
|
|
||||||
|
|
||||||
if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd;
|
|
||||||
if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd;
|
|
||||||
if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart;
|
|
||||||
if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart;
|
|
||||||
|
|
||||||
if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor;
|
|
||||||
if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor;
|
|
||||||
|
|
||||||
const current = { ...prev[selectedTerm], [field]: numValue };
|
|
||||||
|
|
||||||
if (field === 'supportStart') {
|
|
||||||
if (current.supportStart > current.coreStart) current.coreStart = current.supportStart;
|
|
||||||
if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart;
|
|
||||||
if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
|
|
||||||
} else if (field === 'coreStart') {
|
|
||||||
if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
|
|
||||||
if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart;
|
|
||||||
if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
|
|
||||||
} else if (field === 'coreEnd') {
|
|
||||||
if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
|
|
||||||
if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd;
|
|
||||||
if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
|
|
||||||
} else if (field === 'supportEnd') {
|
|
||||||
if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd;
|
|
||||||
if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd;
|
|
||||||
if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...prev, [selectedTerm]: current };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinalSubmit = () => {
|
|
||||||
console.log("PAYLOAD DOC-MF:", { base_scale: baseScale, membership_functions: mfDefinitions });
|
|
||||||
alert("¡Mira la consola! JSON preparado.");
|
|
||||||
};
|
|
||||||
|
|
||||||
const scaleKeys = Object.keys(baseScale);
|
|
||||||
const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb';
|
|
||||||
|
|
||||||
const needsZoom = dimensions.table > dimensions.container;
|
|
||||||
const dynamicScale = needsZoom ? (dimensions.container / dimensions.table) * 0.95 : 1;
|
|
||||||
const currentScale = isZoomActive && needsZoom ? dynamicScale : 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col items-center">
|
|
||||||
|
|
||||||
{/* PASO 1 */}
|
|
||||||
{step === 1 && (
|
|
||||||
<div className="w-full bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-6 flex flex-col items-center animate-fade-in relative overflow-visible">
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center w-full mb-4 border-b pb-3 relative z-30">
|
|
||||||
<h2 className="text-xl font-bold text-slate-800">
|
|
||||||
Paso 1: Establecer escala
|
|
||||||
</h2>
|
|
||||||
{needsZoom && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (containerRef.current) containerRef.current.scrollLeft = 0;
|
|
||||||
setIsZoomActive(!isZoomActive);
|
|
||||||
}}
|
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg font-bold transition-all shadow-sm border text-sm ${isZoomActive ? 'bg-blue-50 border-blue-200 text-blue-700' : 'bg-white border-slate-200 text-slate-600'}`}
|
|
||||||
>
|
|
||||||
<span>{isZoomActive ? '🔍' : '🖼️'}</span>
|
|
||||||
{isZoomActive ? 'Ver de cerca (Scroll)' : 'Ajustar mesa'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CriterionInput criterionName={criterionName} setCriterionName={handleCriterionChange} error={errors.criterion} />
|
|
||||||
|
|
||||||
<div ref={containerRef} className={`w-full mt-2 transition-all relative ${!isZoomActive && needsZoom ? 'overflow-x-auto flex justify-start pb-8 pt-4 px-4' : 'overflow-hidden flex justify-center pb-8 pt-4'}`}>
|
|
||||||
<div className={`flex flex-row items-start min-w-max transition-transform duration-500 ease-out px-4 origin-top`} style={{ transform: `scale(${currentScale})`, marginBottom: isZoomActive && currentScale < 1 ? `-${(1 - currentScale) * 300}px` : '0px' }}>
|
|
||||||
|
|
||||||
<div ref={tableRef} className="flex flex-row items-start relative px-10 overflow-visible">
|
|
||||||
|
|
||||||
{levels.map((level, index) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<div className="flex flex-col items-center mx-2 my-2 relative z-20">
|
|
||||||
<CardEditor index={index} level={level} handleLevelChange={handleLevelChange} handleRemoveLevel={handleRemoveLevel} totalLevels={levels.length} error={errors.levels[index]} canRemove={levels.length > 3} />
|
|
||||||
</div>
|
|
||||||
{index < levels.length - 1 && (
|
|
||||||
<BlankCardsCounter index={index} blankCardsCount={blankCards[index]} handleBlankCardChange={handleBlankCardChange} />
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mx-1 my-2 h-52 flex items-center justify-center">
|
|
||||||
<div className="w-10 h-1 bg-slate-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddLevelButton handleAddLevel={handleAddLevel} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full max-w-lg mt-2 pt-6 border-t border-slate-200 flex flex-col items-center z-20 relative bg-white">
|
|
||||||
<button onClick={handleGenerateBaseScale} disabled={isLoading} className={`w-full py-3 text-white text-lg font-bold rounded-xl shadow-md transition-all active:scale-[0.98] ${isLoading ? 'bg-slate-400 cursor-not-allowed' : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700'}`}>
|
|
||||||
{isLoading ? 'Calculando...' : 'Generar Gráfica Continua'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PASO 2 */}
|
|
||||||
{step === 2 && (
|
|
||||||
<div className="w-full bg-white p-6 rounded-2xl shadow-sm border border-slate-200 animate-fade-in relative overflow-visible">
|
|
||||||
<div className="flex justify-between items-center mb-6 border-b pb-3">
|
|
||||||
<h2 className="text-xl font-bold text-slate-800">Paso 2: Modelar Conceptos Difusos</h2>
|
|
||||||
<button onClick={() => setStep(1)} className="text-slate-500 hover:text-blue-600 text-sm font-semibold underline">← Volver a las cartas</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-3 mb-6">
|
|
||||||
{scaleKeys.map((name, index) => {
|
|
||||||
const color = COLORS[index % COLORS.length];
|
|
||||||
const isSelected = selectedTerm === name;
|
|
||||||
return (
|
|
||||||
<button key={name} onClick={() => setSelectedTerm(name)} style={isSelected ? { backgroundColor: color, borderColor: color, color: '#fff' } : { borderColor: color, color: '#475569' }} className={`px-5 py-2 rounded-lg font-bold border-2 transition-all duration-300 flex flex-col items-center shadow-sm hover:shadow-md ${isSelected ? 'transform scale-105' : 'bg-white opacity-80 hover:opacity-100'}`}>
|
|
||||||
<span>{name}</span><span className="text-[10px] font-normal opacity-80">(X: {baseScale[name].toFixed(2)})</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Chart baseScale={baseScale} mfDefinitions={mfDefinitions} selectedTerm={selectedTerm} colors={COLORS} />
|
|
||||||
|
|
||||||
<Controls selectedTerm={selectedTerm} currentMf={mfDefinitions[selectedTerm]} selectedColor={selectedColor} baseScale={baseScale} mfDefinitions={mfDefinitions} updateCurrentMf={updateCurrentMf} />
|
|
||||||
|
|
||||||
<div className="w-full mt-8 flex justify-center">
|
|
||||||
<button onClick={handleFinalSubmit} className="px-10 py-3 bg-slate-900 text-white text-lg font-bold rounded-xl shadow-md hover:bg-black hover:shadow-lg transition-all">
|
|
||||||
Guardar Todo el Espectro Difuso
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import CriterionInput from '../components/CriterionInput';
|
|
||||||
import CardEditor from '../components/CardEditor';
|
|
||||||
import BlankCardsCounter from '../components/BlankCardsCounter';
|
|
||||||
import AddLevelButton from '../components/AddLevelButton';
|
|
||||||
import ValueFunctionChart from '../components/ValueFunctionChart';
|
|
||||||
import { calculateValueFunction } from '../services/docService';
|
|
||||||
|
|
||||||
export default function BasicMode() {
|
|
||||||
const [criterionName, setCriterionName] = useState('');
|
|
||||||
const [levels, setLevels] = useState(['', '', '']);
|
|
||||||
const [blankCards, setBlankCards] = useState([0, 0]);
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [result, setResult] = useState(null);
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState({ criterion: false, levels: [] });
|
|
||||||
|
|
||||||
const handleCalculate = async () => {
|
|
||||||
|
|
||||||
let hasError = false;
|
|
||||||
const newErrors = { criterion: false, levels: Array(levels.length).fill(false) };
|
|
||||||
|
|
||||||
if (!criterionName.trim()) {
|
|
||||||
newErrors.criterion = true;
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
levels.forEach((level, idx) => {
|
|
||||||
if (!level.trim()) {
|
|
||||||
newErrors.levels[idx] = true;
|
|
||||||
hasError = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
|
|
||||||
if (hasError) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setResult(null);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
criterion_name: criterionName.trim(),
|
|
||||||
levels: levels.map(l => l.trim()),
|
|
||||||
blank_cards: blankCards,
|
|
||||||
references: { "0": 0, [(levels.length - 1).toString()]: 1 }
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await calculateValueFunction(payload);
|
|
||||||
setResult(data);
|
|
||||||
} catch (error) {
|
|
||||||
alert("No se ha podido conectar con el backend: " + error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCriterionChange = (val) => {
|
|
||||||
setCriterionName(val);
|
|
||||||
if (errors.criterion) setErrors({ ...errors, criterion: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLevelChange = (index, newValue) => {
|
|
||||||
const newLevels = [...levels];
|
|
||||||
newLevels[index] = newValue;
|
|
||||||
setLevels(newLevels);
|
|
||||||
|
|
||||||
if (errors.levels[index]) {
|
|
||||||
const newErrLevels = [...errors.levels];
|
|
||||||
newErrLevels[index] = false;
|
|
||||||
setErrors({ ...errors, levels: newErrLevels });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddLevel = () => {
|
|
||||||
setLevels([...levels, '']);
|
|
||||||
setBlankCards([...blankCards, 0]);
|
|
||||||
setErrors({ ...errors, levels: [...errors.levels, false] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveLevel = (indexToRemove) => {
|
|
||||||
if (levels.length <= 3) return;
|
|
||||||
const newLevels = levels.filter((_, index) => index !== indexToRemove);
|
|
||||||
const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1;
|
|
||||||
const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove);
|
|
||||||
|
|
||||||
const newErrLevels = errors.levels.filter((_, index) => index !== indexToRemove);
|
|
||||||
|
|
||||||
setLevels(newLevels);
|
|
||||||
setBlankCards(newBlankCards);
|
|
||||||
setErrors({ ...errors, levels: newErrLevels });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlankCardChange = (index, delta) => {
|
|
||||||
const newBlankCards = [...blankCards];
|
|
||||||
const newValue = newBlankCards[index] + delta;
|
|
||||||
if (newValue >= 0) {
|
|
||||||
newBlankCards[index] = newValue;
|
|
||||||
setBlankCards(newBlankCards);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col items-center">
|
|
||||||
|
|
||||||
<CriterionInput
|
|
||||||
criterionName={criterionName}
|
|
||||||
setCriterionName={handleCriterionChange}
|
|
||||||
error={errors.criterion}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="w-full max-w-lg flex flex-col items-center">
|
|
||||||
{levels.map((level, index) => (
|
|
||||||
<div key={index} className="w-full flex flex-col items-center">
|
|
||||||
|
|
||||||
<CardEditor
|
|
||||||
index={index}
|
|
||||||
level={level}
|
|
||||||
handleLevelChange={handleLevelChange}
|
|
||||||
handleRemoveLevel={handleRemoveLevel}
|
|
||||||
totalLevels={levels.length}
|
|
||||||
error={errors.levels[index]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{index < levels.length - 1 && (
|
|
||||||
<BlankCardsCounter
|
|
||||||
index={index}
|
|
||||||
blankCardsCount={blankCards[index]}
|
|
||||||
handleBlankCardChange={handleBlankCardChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<AddLevelButton handleAddLevel={handleAddLevel} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full max-w-lg mt-12 pt-8 border-t-2 border-slate-200 flex flex-col items-center">
|
|
||||||
<button
|
|
||||||
onClick={handleCalculate}
|
|
||||||
disabled={isLoading}
|
|
||||||
className={`w-full py-4 text-white text-xl font-bold rounded-xl shadow-lg transition-all active:scale-[0.98] ${
|
|
||||||
isLoading ? 'bg-slate-400 cursor-not-allowed' : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 hover:shadow-xl'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Calculando...' : 'Calcular Valores DoC'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ValueFunctionChart result={result} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { authService } from '../services/authService';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await authService.login(email, password);
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: data.user_id,
|
||||||
|
username: data.username,
|
||||||
|
email: email
|
||||||
|
};
|
||||||
|
|
||||||
|
login(userData, data.token);
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.detail || 'Error al iniciar sesión. Revisa tus credenciales.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center mt-4 sm:mt-8">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-2xl shadow-sm border border-slate-200 p-8">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-800">Bienvenido</h2>
|
||||||
|
<p className="text-slate-500 mt-2">Inicia sesión para guardar tus espectros difusos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm font-medium border border-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Contraseña</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 transition-colors shadow-md mt-4"
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="text-center mt-6 text-sm text-slate-600">
|
||||||
|
¿No tienes cuenta? <Link to="/register" className="text-blue-600 font-bold hover:underline">Regístrate aquí</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { authService } from '../services/authService';
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await authService.register(username, email, password);
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: data.user_id,
|
||||||
|
username: username,
|
||||||
|
email: email
|
||||||
|
};
|
||||||
|
|
||||||
|
login(userData, data.token);
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.detail || 'Error al registrar el usuario.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center mt-4 sm:mt-8">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-2xl shadow-sm border border-slate-200 p-8">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-slate-800">Crear Cuenta</h2>
|
||||||
|
<p className="text-slate-500 mt-2">Únete para guardar tu progreso</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg mb-6 text-sm font-medium border border-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Nombre de usuario</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
autoComplete="username"
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Ej: alexis99"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Contraseña</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 transition-colors shadow-md mt-4"
|
||||||
|
>
|
||||||
|
Registrarse
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="text-center mt-6 text-sm text-slate-600">
|
||||||
|
¿Ya tienes cuenta? <Link to="/login" className="text-blue-600 font-bold hover:underline">Inicia sesión aquí</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import MainLayout from '../components/layout/MainLayout';
|
import MainLayout from '../components/layout/MainLayout';
|
||||||
import DocEditor from '../pages/DocEditor';
|
import DocEditor from '../pages/DocEditor';
|
||||||
|
import Login from '../pages/Login';
|
||||||
|
import Register from '../pages/Register';
|
||||||
|
|
||||||
export function AppRouter() {
|
export function AppRouter() {
|
||||||
return (
|
return (
|
||||||
@@ -8,6 +10,8 @@ export function AppRouter() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<MainLayout />}>
|
<Route path="/" element={<MainLayout />}>
|
||||||
<Route index element={<DocEditor />} />
|
<Route index element={<DocEditor />} />
|
||||||
|
<Route path="login" element={<Login />} />
|
||||||
|
<Route path="register" element={<Register />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import api from '../lib/api';
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
login: async (email, password) => {
|
||||||
|
const response = await api.post('/auth/login', { email, password });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
register: async (username, email, password) => {
|
||||||
|
const response = await api.post('/auth/register', { username, email, password });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user