diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index c8bc184..cd5bf25 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -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() { + const { user, isAuthenticated, logout } = useAuth(); + return ( -
- - {/* Cabecera */} -
-
-
- DoC +
+ +
+
+ + +
+ DoC +
+ + Deck of Cards + + + +
+ {isAuthenticated ? ( +
+
{ + if(window.confirm('¿Deseas cerrar sesión?')) { + logout(); + } + }} + > + {user?.username?.charAt(0).toUpperCase() || 'U'} +
+
+ ) : ( + + Iniciar Sesión + + )}
-

- Deck of Cards -

+
- {/* Contenido principal */} -
+
-
); } \ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index dae5d65..787616f 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -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; \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx deleted file mode 100644 index 88a5a83..0000000 --- a/frontend/src/pages/AdvancedMode.jsx +++ /dev/null @@ -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 ( -
- - {/* PASO 1 */} - {step === 1 && ( -
- -
-

- Paso 1: Establecer escala -

- {needsZoom && ( - - )} -
- - - -
-
- -
- - {levels.map((level, index) => ( - -
- 3} /> -
- {index < levels.length - 1 && ( - - )} -
- ))} - -
-
-
- - -
- -
-
- -
- -
-
- )} - - {/* PASO 2 */} - {step === 2 && ( -
-
-

Paso 2: Modelar Conceptos Difusos

- -
- -
- {scaleKeys.map((name, index) => { - const color = COLORS[index % COLORS.length]; - const isSelected = selectedTerm === name; - return ( - - ); - })} -
- - - - - -
- -
-
- )} -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx deleted file mode 100644 index 3d913e6..0000000 --- a/frontend/src/pages/BasicMode.jsx +++ /dev/null @@ -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 ( -
- - - -
- {levels.map((level, index) => ( -
- - - - {index < levels.length - 1 && ( - - )} -
- ))} - - -
- -
- -
- - - -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..e6a48ff --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -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 ( +
+
+
+

Bienvenido

+

Inicia sesión para guardar tus espectros difusos

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ + +
+ +

+ ¿No tienes cuenta? Regístrate aquí +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..3055c9d --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -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 ( +
+
+
+

Crear Cuenta

+

Únete para guardar tu progreso

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="Ej: alexis99" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ + +
+ +

+ ¿Ya tienes cuenta? Inicia sesión aquí +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 5cc0534..679ca28 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,6 +1,8 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '../components/layout/MainLayout'; import DocEditor from '../pages/DocEditor'; +import Login from '../pages/Login'; +import Register from '../pages/Register'; export function AppRouter() { return ( @@ -8,6 +10,8 @@ export function AppRouter() { }> } /> + } /> + } /> } /> diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..be72b64 --- /dev/null +++ b/frontend/src/services/authService.js @@ -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; + } +}; \ No newline at end of file