From e96af43990baeff60d86c6b3246833526dc397f2 Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 14:27:50 +0100 Subject: [PATCH 01/23] add: authContext implementado --- frontend/src/context/AuthContext.js | 7 ++++++ frontend/src/context/AuthProvider.jsx | 32 +++++++++++++++++++++++++++ frontend/src/main.jsx | 7 ++++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 frontend/src/context/AuthContext.js create mode 100644 frontend/src/context/AuthProvider.jsx diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js new file mode 100644 index 0000000..ac7e88c --- /dev/null +++ b/frontend/src/context/AuthContext.js @@ -0,0 +1,7 @@ +import { createContext, useContext } from 'react'; + +export const AuthContext = createContext(); + +export const useAuth = () => { + return useContext(AuthContext); +}; \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.jsx b/frontend/src/context/AuthProvider.jsx new file mode 100644 index 0000000..5180fb6 --- /dev/null +++ b/frontend/src/context/AuthProvider.jsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { AuthContext } from './AuthContext'; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(() => { + const storedUser = localStorage.getItem('user'); + return storedUser ? JSON.parse(storedUser) : null; + }); + + const login = (userData, token) => { + setUser(userData); + localStorage.setItem('user', JSON.stringify(userData)); + localStorage.setItem('token', token); + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user'); + localStorage.removeItem('token'); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b9a1a6d..33c21e7 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { AuthProvider } from './context/AuthProvider.jsx' createRoot(document.getElementById('root')).render( - + + + , -) +) \ No newline at end of file From 22ed6c107e7f0cef9077410fb8502dc33af8c4cc Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 6 Apr 2026 10:20:27 +0200 Subject: [PATCH 02/23] add: implementar login/registro --- frontend/src/components/layout/MainLayout.jsx | 57 +++-- frontend/src/lib/api.js | 7 + frontend/src/pages/AdvancedMode.jsx | 236 ------------------ frontend/src/pages/BasicMode.jsx | 156 ------------ frontend/src/pages/Login.jsx | 88 +++++++ frontend/src/pages/Register.jsx | 102 ++++++++ frontend/src/routers/AppRouter.jsx | 4 + frontend/src/services/authService.js | 13 + 8 files changed, 257 insertions(+), 406 deletions(-) delete mode 100644 frontend/src/pages/AdvancedMode.jsx delete mode 100644 frontend/src/pages/BasicMode.jsx create mode 100644 frontend/src/pages/Login.jsx create mode 100644 frontend/src/pages/Register.jsx create mode 100644 frontend/src/services/authService.js 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 From 2a237a51db0a57b0de83695d5d1a4140d78efee0 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 6 Apr 2026 12:34:42 +0200 Subject: [PATCH 03/23] =?UTF-8?q?add:=20a=C3=B1adir=20modo=20rango=20en=20?= =?UTF-8?q?el=20modal=20para=20la=20subescala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/SubscaleModal.jsx | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 70bb4ce..0980b37 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -4,11 +4,20 @@ import BlankCardsCounter from '../BlankCardsCounter'; export default function SubscaleModal({ onClose, onSave, targetInfo }) { const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); - const [blankCards, setBlankCards] = useState(targetInfo?.initialData?.blankCards || [0]); + + const [blankCards, setBlankCards] = useState(() => { + const initialBlanks = targetInfo?.initialData?.blankCards || [0]; + return initialBlanks.map(b => { + if (Array.isArray(b)) { + return { min: b[0], max: b[1], isRange: true }; + } + return { min: b, max: b, isRange: false }; + }); + }); const handleAddCard = () => { setCardsCount(prev => prev + 1); - setBlankCards([...blankCards, 0]); + setBlankCards([...blankCards, { min: 0, max: 0, isRange: false }]); }; const handleRemoveCard = () => { @@ -17,16 +26,46 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { setBlankCards(blankCards.slice(0, -1)); }; - const handleBlankCardChange = (index, delta) => { + const handleExactChange = (index, delta) => { const newBlanks = [...blankCards]; - if (newBlanks[index] + delta >= 0) { - newBlanks[index] += delta; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0) { + newBlanks[index].min = newVal; + newBlanks[index].max = newVal; setBlankCards(newBlanks); } }; + const handleMinChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0 && newVal <= newBlanks[index].max) { + newBlanks[index].min = newVal; + setBlankCards(newBlanks); + } + }; + + const handleMaxChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].max + delta; + if (newVal >= newBlanks[index].min) { + newBlanks[index].max = newVal; + setBlankCards(newBlanks); + } + }; + + const toggleRangeMode = (index) => { + const newBlanks = [...blankCards]; + newBlanks[index].isRange = !newBlanks[index].isRange; + if (!newBlanks[index].isRange) { + newBlanks[index].max = newBlanks[index].min; + } + setBlankCards(newBlanks); + }; + const handleSave = () => { - onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards }); + const payloadBlanks = blankCards.map(b => b.isRange ? [b.min, b.max] : b.min); + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards: payloadBlanks }); }; const handleDelete = () => { @@ -35,7 +74,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { return (
-
+
@@ -47,34 +86,83 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
- {/* Tablero */} + {/* Tablero de Cartas */}
+ {Array.from({ length: cardsCount }).map((_, index) => ( + {/* CARTA DE REFERENCIA */}
{cardsCount > 2 && index === cardsCount - 1 && ( - + )} {index + 1}
+ + {/* HUECO ENTRE CARTAS: Representación y Controles */} {index < cardsCount - 1 && ( - +
+ + {/* Representación visual de las cartas blancas Sólidas / Fantasmas */} +
+ {Array.from({ length: blankCards[index].min }).map((_, i) => ( +
+ ))} + {blankCards[index].isRange && Array.from({ length: blankCards[index].max - blankCards[index].min }).map((_, i) => ( +
+ ? +
+ ))} +
+ + {/* Controles de números */} + {blankCards[index].isRange ? ( + // MODO RANGO +
+
+ MÍN + handleMinChange(idx, delta)} /> +
+
+ MÁX + handleMaxChange(idx, delta)} /> +
+
+ ) : ( + // MODO EXACTO +
+ CARTAS + handleExactChange(idx, delta)} /> +
+ )} + + {/* Botón Toggle */} + + +
)} ))} + {/* Botón Añadir Carta */}
-
+
- {/* Botones */} + {/* Botones de Acción */}
- {/* Lado derecho (Pendiente descendente) */}
-
- - updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> -
updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
- {/* Botón subescala derecha */}
+ + {/* Dropdown Menu (Solo Usuario y Cerrar Sesión) */} + {isDropdownOpen && ( + <> +
setIsDropdownOpen(false)} + >
+ +
+
+

Usuario

+

{user?.username}

+
+ + +
+ + )}
) : ( - - Iniciar Sesión - + // BOTONES PARA USUARIO NO LOGUEADO +
+ + Iniciar sesión + + + Registrarse + +
)}
-
-
- + {/* CONTENIDO PRINCIPAL */} +
+ {children}
); diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 1ebf5f9..dd393b5 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; -import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; +import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService'; import Step3FinalGraph from '../components/editor/Step3FinalGraph'; export default function DocEditor() { @@ -193,6 +193,38 @@ export default function DocEditor() { } }; + // Petición para guardar en el historial + const handleSaveToHistory = async () => { + const token = localStorage.getItem('token'); + if (!token) { + alert("Para guardar tu modelo debes iniciar sesión primero. Puedes seguir visualizando la gráfica sin problema."); + return; + } + + const defaultName = criterionName ? `Modelo de ${criterionName}` : "Mi nueva gráfica DoC-IT2MF"; + const historyName = prompt("Dale un nombre a este modelo para guardarlo en tu historial:", defaultName); + + if (!historyName) return; + + setIsLoading(true); + try { + const payload = { + name: historyName, + results: finalResult.levels || finalResult.results + }; + + await saveToHistory(payload); + + alert("¡Gráfica guardada con éxito en tu historial!"); + + } catch (error) { + console.error("Error al guardar en el historial:", error); + alert("Hubo un problema al guardar el modelo: " + error); + } finally { + setIsLoading(false); + } + }; + return (
@@ -223,10 +255,13 @@ export default function DocEditor() {
)} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 0000000..e5df8c4 --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { getUserHistory, deleteHistoryItem } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; + +export default function History() { + const [historyItems, setHistoryItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + fetchHistory(); + }, []); + + const fetchHistory = async () => { + setIsLoading(true); + try { + const data = await getUserHistory(); + const items = Array.isArray(data) ? data : data.history || data.items || []; + + setHistoryItems(items.reverse()); + } catch (error) { + console.error("Error fetching history:", error); + alert("Hubo un problema al cargar el historial."); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('¿Seguro que quieres borrar este modelo definitivamente?')) return; + + try { + await deleteHistoryItem(id); + setHistoryItems(prev => prev.filter(item => item._id !== id && item.id !== id)); + if (expandedId === id) setExpandedId(null); + } catch (error) { + alert("Error al borrar: " + error); + } + }; + + const toggleExpand = (id) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+ + {/* Cabecera */} +
+
+

Mi Historial

+

+ Aquí están todas las gráficas y modelos que has guardado. +

+
+ + + Nuevo Modelo + +
+ + {/* Lista de Historial */} + {isLoading ? ( +
+
+

Cargando tus gráficas...

+
+ ) : historyItems.length === 0 ? ( +
+ 📭 +

Aún no has guardado ningún modelo.

+

Ve al editor, crea una gráfica y dale a "Finalizar y Guardar".

+
+ ) : ( +
+ {historyItems.map((item) => { + const itemId = item._id || item.id; + const isExpanded = expandedId === itemId; + + return ( +
+ + {/* Cabecera de la Card (Siempre visible) */} +
+
+
+ 📊 +
+
+

{item.name || 'Modelo sin título'}

+

+ Guardado en el historial +

+
+
+ +
+ + +
+
+ + {/* Contenido Desplegable (La Gráfica) */} + {isExpanded && ( +
+ +
+ )} +
+ ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 679ca28..3337675 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,20 +1,22 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { BrowserRouter as Router, 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'; +import History from '../pages/History'; -export function AppRouter() { +export default function AppRouter() { return ( - - - }> - } /> - } /> - } /> - } /> - - - + + + + } /> + } /> + } /> + } /> + } /> + + + ); } \ No newline at end of file diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js index 16ea154..ecf3686 100644 --- a/frontend/src/services/docService.js +++ b/frontend/src/services/docService.js @@ -18,4 +18,34 @@ export const buildFuzzyGraph = async (payload) => { console.error('Error building fuzzy graph:', error); throw error.response?.data?.detail || error.message; } +}; + +export const saveToHistory = async (payload) => { + try { + const response = await api.post('/history/add', payload); + return response.data; + } catch (error) { + console.error('Error saving to history:', error); + throw error.response?.data?.detail || error.message; + } +}; + +export const getUserHistory = async () => { + try { + const response = await api.get('/history/user'); + return response.data; + } catch (error) { + console.error('Error fetching history:', error); + throw error.response?.data?.detail || error.message; + } +}; + +export const deleteHistoryItem = async (id) => { + try { + const response = await api.delete(`/history/delete/${id}`); + return response.data; + } catch (error) { + console.error('Error deleting history item:', error); + throw error.response?.data?.detail || error.message; + } }; \ No newline at end of file From 62a4db33a661dad83d77949426509b0471d66ee8 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 09:39:48 +0200 Subject: [PATCH 06/23] =?UTF-8?q?refactor:=20mejorar=20dise=C3=B1o=20de=20?= =?UTF-8?q?interfaz=20y=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/layout/MainLayout.jsx | 14 ++++++++++---- frontend/src/pages/History.jsx | 14 ++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 0a127ec..2c164a0 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -102,11 +102,17 @@ export default function MainLayout({ children }) { ) : ( // BOTONES PARA USUARIO NO LOGUEADO -
- - Iniciar sesión +
+ + Entrar - + Registrarse
diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index e5df8c4..12da8e0 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -44,8 +44,7 @@ export default function History() { }; return ( -
- +
{/* Cabecera */}
@@ -83,7 +82,7 @@ export default function History() { return (
- {/* Cabecera de la Card (Siempre visible) */} + {/* Cabecera de la Card */}
@@ -92,7 +91,10 @@ export default function History() {

{item.name || 'Modelo sin título'}

- Guardado en el historial + {item.created_at + ? `Guardado el ${new Date(item.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric' })}` + : 'Guardado en el historial' + }

@@ -109,12 +111,12 @@ export default function History() { className="px-4 py-2.5 bg-white border border-red-200 text-red-500 font-bold rounded-xl hover:bg-red-50 transition-colors shadow-sm" title="Borrar modelo" > - 🗑️ + Borrar
- {/* Contenido Desplegable (La Gráfica) */} + {/* Contenido Desplegable (La gráfica) */} {isExpanded && (
From 66c350c8a49ecf0b0f43647d4b38732de2fad4e3 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 10:04:34 +0200 Subject: [PATCH 07/23] =?UTF-8?q?add:=20a=C3=B1adir=20logo=20y=20favicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/favicon.svg | 2 +- frontend/src/components/layout/MainLayout.jsx | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..38aaf3c 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file +logo-doc \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 2c164a0..9ff0053 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -29,8 +29,15 @@ export default function MainLayout({ children }) {
{/* Logo / Título */} - - + + Deck of Cards Logo + + {/* Texto del título */} + Deck of Cards From 392a1fb36c5670407c0fc67d88d614b7ee722673 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 10:48:22 +0200 Subject: [PATCH 08/23] proceso rangos --- .../src/components/editor/SubscaleModal.jsx | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 0980b37..3a751f1 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -102,49 +102,51 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
- {/* HUECO ENTRE CARTAS: Representación y Controles */} + {/* HUECO ENTRE CARTAS */} {index < cardsCount - 1 && ( -
- - {/* Representación visual de las cartas blancas Sólidas / Fantasmas */} -
- {Array.from({ length: blankCards[index].min }).map((_, i) => ( -
- ))} - {blankCards[index].isRange && Array.from({ length: blankCards[index].max - blankCards[index].min }).map((_, i) => ( -
- ? -
- ))} -
+
{/* Controles de números */} {blankCards[index].isRange ? ( - // MODO RANGO -
+ // MODO RANGO +
- MÍN - handleMinChange(idx, delta)} /> + MÍN + handleMinChange(idx, delta)} + />
+ {/* Guión separador para unificar visualmente el rango */} + -
- MÁX - handleMaxChange(idx, delta)} /> + MÁX + handleMaxChange(idx, delta)} + />
) : ( // MODO EXACTO -
- CARTAS - handleExactChange(idx, delta)} /> +
+ CARTAS + handleExactChange(idx, delta)} + />
)} {/* Botón Toggle */}
From 9602c4f5096ce3909c3aeead2b3ab88d79f1b209 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 12:47:13 +0200 Subject: [PATCH 09/23] =?UTF-8?q?refactor:=20mejorar=20dise=C3=B1o=20y=20f?= =?UTF-8?q?uncionalidad=20de=20componentes=20en=20el=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/BlankCardsCounter.jsx | 40 +++--- frontend/src/components/CardEditor.jsx | 4 +- .../src/components/editor/Step1BaseScale.jsx | 32 +++-- .../components/editor/Step2FuzzyModeling.jsx | 2 +- .../src/components/editor/SubscaleModal.jsx | 115 ++++++++++-------- 5 files changed, 107 insertions(+), 86 deletions(-) diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index b35b7ed..3292eea 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -1,3 +1,5 @@ +import React from 'react'; + export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { const maxCardsPerRow = 7; @@ -7,35 +9,29 @@ export default function BlankCardsCounter({ index, blankCardsCount, handleBlankC } return ( -
+
-
+ {/* Bloque de botones */} +
+ - {/* Línea conectora horizontal */} -
- - {/* Botones - y + */} -
- - -
- Blancas - {blankCardsCount} -
- - +
+ Blancas + {blankCardsCount}
+ +
{/* Cartas blancas */} {blankCardsCount > 0 && ( -
+
{rows.map((row, rowIndex) => (
{row.map((_, colIndex) => ( diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index c5e887f..4783869 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -10,9 +10,9 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemo )} {index + 1} {index + 1} - handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} /> + handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} />
-
{error &&

Escribe una etiqueta

}
+
{error &&

Escribe un término

}
); } \ No newline at end of file diff --git a/frontend/src/components/editor/Step1BaseScale.jsx b/frontend/src/components/editor/Step1BaseScale.jsx index 115b450..2ee5755 100644 --- a/frontend/src/components/editor/Step1BaseScale.jsx +++ b/frontend/src/components/editor/Step1BaseScale.jsx @@ -59,30 +59,48 @@ export default function Step1BaseScale({ -
+
{levels.map((level, index) => ( -
+ + {/* CARTA DE NIVEL */} +
3} />
+ + {/* HUECO ENTRE CARTAS Y CONTADOR */} {index < levels.length - 1 && ( - +
+
+ +
+ +
+
)} ))} -
-
+ + {/* LÍNEA HACIA EL BOTÓN DE AÑADIR */} +
+
- + + {/* BOTÓN AÑADIR NIVEL */} +
+ +
+
-
+ {/* Generar Gráfica Continua */} +
diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index dcc3d18..2369c54 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -79,7 +79,7 @@ export default function Step2FuzzyModeling({
diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 3a751f1..6f70c3a 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -3,10 +3,19 @@ import BlankCardsCounter from '../BlankCardsCounter'; export default function SubscaleModal({ onClose, onSave, targetInfo }) { - const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); + const initialCount = Math.max(3, targetInfo?.initialData?.cardsCount || 3); + const [cardsCount, setCardsCount] = useState(initialCount); const [blankCards, setBlankCards] = useState(() => { - const initialBlanks = targetInfo?.initialData?.blankCards || [0]; + let initialBlanks = targetInfo?.initialData?.blankCards; + + if (!initialBlanks || initialBlanks.length === 0) { + initialBlanks = [0, 0]; + } else if (initialBlanks.length < initialCount - 1) { + const padding = Array(initialCount - 1 - initialBlanks.length).fill(0); + initialBlanks = [...initialBlanks, ...padding]; + } + return initialBlanks.map(b => { if (Array.isArray(b)) { return { min: b[0], max: b[1], isRange: true }; @@ -21,7 +30,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { }; const handleRemoveCard = () => { - if (cardsCount <= 2) return; + if (cardsCount <= 3) return; setCardsCount(prev => prev - 1); setBlankCards(blankCards.slice(0, -1)); }; @@ -73,10 +82,10 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { }; return ( -
-
+
+
-
+

Diseñar Subescala

@@ -86,16 +95,15 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {

- {/* Tablero de Cartas */} -
+
{Array.from({ length: cardsCount }).map((_, index) => ( {/* CARTA DE REFERENCIA */} -
+
- {cardsCount > 2 && index === cardsCount - 1 && ( + {cardsCount > 3 && index === cardsCount - 1 && ( )} {index + 1} @@ -104,58 +112,57 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { {/* HUECO ENTRE CARTAS */} {index < cardsCount - 1 && ( -
- - {/* Controles de números */} - {blankCards[index].isRange ? ( - // MODO RANGO -
-
- MÍN - handleMinChange(idx, delta)} - /> +
+
+ +
+ {blankCards[index].isRange ? ( +
+
+ MÍN + handleMinChange(idx, delta)} + /> +
+ +
-
+ +
+ MÁX + handleMaxChange(idx, delta)} + /> +
- {/* Guión separador para unificar visualmente el rango */} - - -
- MÁX - handleMaxChange(idx, delta)} - /> + ) : ( +
+ CARTAS + handleExactChange(idx, delta)} + />
-
- ) : ( - // MODO EXACTO -
- CARTAS - handleExactChange(idx, delta)} - /> -
- )} - - {/* Botón Toggle */} - + )} + +
)} ))} {/* Botón Añadir Carta */} -
+
@@ -165,7 +172,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
{/* Botones de Acción */} -
+
From 5fbf08cdc192f8ac5a053fb0649fb6bea6c11a26 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 13:02:13 +0200 Subject: [PATCH 10/23] refactor: mejorar logo favicon --- frontend/public/favicon.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 38aaf3c..cd55914 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ -logo-doc \ No newline at end of file +logo doc \ No newline at end of file From 03e3b69ae3902d258547c58c2a1170fc72264711 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 9 Apr 2026 10:50:43 +0200 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20cambios=20m=C3=ADnimos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 2 +- frontend/src/pages/History.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index dd393b5..1ea2b5b 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -261,7 +261,7 @@ export default function DocEditor() { isLoading ? 'bg-slate-400 text-slate-100 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700' }`} > - {isLoading ? 'Guardando...' : 'Finalizar y Guardar'} + {isLoading ? 'Guardando...' : 'Guardar'}
)} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 12da8e0..10529fa 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -71,7 +71,7 @@ export default function History() {
📭

Aún no has guardado ningún modelo.

-

Ve al editor, crea una gráfica y dale a "Finalizar y Guardar".

+

Ve al editor, crea una gráfica y dale a "Guardar".

) : (
From d3e44a624977a4666188eef32660a87bd2460aca Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 9 Apr 2026 13:00:46 +0200 Subject: [PATCH 12/23] =?UTF-8?q?refactor:=20optimizar=20la=20generaci?= =?UTF-8?q?=C3=B3n=20de=20datos=20en=20el=20gr=C3=A1fico=20final,=20mostra?= =?UTF-8?q?ndo=20el=20grado=20de=20pertenencia=20en=20cualquier=20punto=20?= =?UTF-8?q?de=20la=20gr=C3=A1fica.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 226 +++++++++++++++--- 1 file changed, 192 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 99884dd..fd6edf6 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -2,87 +2,245 @@ import React, { useMemo } from 'react'; import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { CHART_COLORS } from '../../config'; +// 1. Función auxiliar +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) { + return nodes[i][1]; + } + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + + if (Math.abs(x2 - x1) < EPSILON) continue; + + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + const Step3FinalGraph = ({ data, criterionName }) => { + + // Extracción de Nodos Base const sortedResults = useMemo(() => { const rawItems = data?.levels || data?.results || []; const processed = rawItems.map((item, index) => { const isType2 = !!item.lower && !!item.upper; const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; - - let lineData = []; - let coreVal = 0; let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; if (isType2) { - const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])]; - const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])]; - - lineData = lowerNodes.map((lNode, i) => { - const uNode = upperNodes[i]; - const lowerY = Number(lNode[1]); - const upperY = Number(uNode ? uNode[1] : lNode[1]); - return { x: Number(lNode[0]), lowerY, upperY, range: [lowerY, upperY] }; - }); - coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; } else { - const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])]; - lineData = nodes.map(node => ({ x: Number(node[0]), y: Number(node[1]) })); - coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; } - - return { ...item, term: termName, isType2, lineData, color, coreVal }; }); return processed.sort((a, b) => a.coreVal - b.coreVal); }, [data]); + // Generación inteligente de datos + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + + for (let i = 0; i <= steps; i++) { + xSet.add(Number((i / steps).toFixed(4))); + } + + sortedResults.forEach(item => { + const addNodes = (nodes) => { + nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + }; + if (item.isType2) { + addNodes(item.lowerNodes); + addNodes(item.upperNodes); + } else { + addNodes(item.nodes); + } + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + + const dataPoints = []; + xValues.forEach(x => { + const point = { x }; + + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + + if (lowerRaw === null && upperRaw === null) { + point[`${item.term}_range`] = null; + } else { + point[`${item.term}_range`] = [lowerRaw !== null ? lowerRaw : 0, upperRaw !== null ? upperRaw : 0]; + } + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + dataPoints.push(point); + }); + return dataPoints; + }, [sortedResults]); + + // Tooltip + const renderCustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => { + if (item.isType2) { + return dataPoint[`${item.term}_upper`] !== null && dataPoint[`${item.term}_upper`] > 0; + } else { + return dataPoint[item.term] !== null && dataPoint[item.term] > 0; + } + }); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: + {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] !== null ? dataPoint[`${item.term}_lower`] : 0; + const upper = dataPoint[`${item.term}_upper`] !== null ? dataPoint[`${item.term}_upper`] : 0; + const range = Math.abs(upper - lower); + + if (range <= 0.001) { + return ( +
+ {item.term} + + Pertenencia: {Number(upper).toFixed(3)} + +
+ ); + } + + return ( +
+ {item.term} + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + +
+ ); + } else { + const val = dataPoint[item.term]; + return ( +
+ {item.term} + + Pertenencia: {Number(val).toFixed(3)} + +
+ ); + } + })} +
+
+ ); + } + return null; + }; + if (!data || (!data.levels && !data.results)) { return

Cargando gráfico final...

; } return ( -
+
+ + - {/* Título */}

{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}

- {/* Gráfica */}
- + - - Array.isArray(value) ? [`[${Number(value[0]).toFixed(3)}, ${Number(value[1]).toFixed(3)}]`, name] : [Number(value).toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} /> + + {sortedResults.map((item) => { if (item.isType2) { return ( - - - + + + ); } else { - return ; + return ; } })}
- {/* Leyenda */}
{sortedResults.map((item) => (
From b6402f2d59f16f7486625ac3004905c5bc2d8588 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 09:56:38 +0200 Subject: [PATCH 13/23] =?UTF-8?q?refactor:=20mejorar=20la=20estructura=20y?= =?UTF-8?q?=20funcionalidad=20del=20gr=C3=A1fico=20final,=20optimizando=20?= =?UTF-8?q?la=20carga=20de=20datos=20y=20la=20visualizaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 302 ++++-------------- .../editor/finalGraph/GraphTooltip.jsx | 53 +++ .../editor/finalGraph/useGraphData.js | 89 ++++++ frontend/src/pages/History.jsx | 21 +- 4 files changed, 226 insertions(+), 239 deletions(-) create mode 100644 frontend/src/components/editor/finalGraph/GraphTooltip.jsx create mode 100644 frontend/src/components/editor/finalGraph/useGraphData.js diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index fd6edf6..49d3e47 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -1,256 +1,92 @@ -import React, { useMemo } from 'react'; +import React, { useState, useEffect, memo } from 'react'; import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -import { CHART_COLORS } from '../../config'; +import { useGraphData } from './finalGraph/useGraphData'; +import { GraphTooltip } from './finalGraph/GraphTooltip'; -// 1. Función auxiliar -const interpolateY = (x, nodes) => { - if (!nodes || nodes.length === 0) return null; - const EPSILON = 1e-5; - const MICRO_STEP = 0.0001; +const Step3FinalGraph = memo(({ data, criterionName }) => { + const { sortedResults, denseData } = useGraphData(data); + const [isReady, setIsReady] = useState(false); - const firstX = nodes[0][0]; - const lastX = nodes[nodes.length - 1][0]; - - if (x < firstX - MICRO_STEP - EPSILON) return null; - if (x > lastX + MICRO_STEP + EPSILON) return null; - - if (x < firstX - EPSILON) return 0; - if (x > lastX + EPSILON) return 0; - - for (let i = nodes.length - 1; i >= 0; i--) { - if (Math.abs(nodes[i][0] - x) < EPSILON) { - return nodes[i][1]; - } - } - - for (let i = 0; i < nodes.length - 1; i++) { - const x1 = nodes[i][0]; - const x2 = nodes[i + 1][0]; - - if (Math.abs(x2 - x1) < EPSILON) continue; - - if (x >= x1 && x <= x2) { - const y1 = nodes[i][1]; - const y2 = nodes[i + 1][1]; - return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); - } - } - return null; -}; - -const Step3FinalGraph = ({ data, criterionName }) => { - - // Extracción de Nodos Base - const sortedResults = useMemo(() => { - const rawItems = data?.levels || data?.results || []; - - const processed = rawItems.map((item, index) => { - const isType2 = !!item.lower && !!item.upper; - const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; - let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; - - if (isType2) { - const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; - return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; - } else { - const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; - return { ...item, term: termName, isType2, nodes, color, coreVal }; - } - }); - - return processed.sort((a, b) => a.coreVal - b.coreVal); - }, [data]); - - // Generación inteligente de datos - const denseData = useMemo(() => { - const xSet = new Set(); - const steps = 1000; - - for (let i = 0; i <= steps; i++) { - xSet.add(Number((i / steps).toFixed(4))); - } - - sortedResults.forEach(item => { - const addNodes = (nodes) => { - nodes.forEach(n => { - const x = n[0]; - xSet.add(Number((x - 0.0001).toFixed(4))); - xSet.add(Number(x.toFixed(4))); - xSet.add(Number((x + 0.0001).toFixed(4))); - }); - }; - if (item.isType2) { - addNodes(item.lowerNodes); - addNodes(item.upperNodes); - } else { - addNodes(item.nodes); - } - }); - - const xValues = Array.from(xSet).sort((a, b) => a - b); - - const dataPoints = []; - xValues.forEach(x => { - const point = { x }; - - sortedResults.forEach(item => { - if (item.isType2) { - const lowerRaw = interpolateY(x, item.lowerNodes); - const upperRaw = interpolateY(x, item.upperNodes); - - point[`${item.term}_lower`] = lowerRaw; - point[`${item.term}_upper`] = upperRaw; - - if (lowerRaw === null && upperRaw === null) { - point[`${item.term}_range`] = null; - } else { - point[`${item.term}_range`] = [lowerRaw !== null ? lowerRaw : 0, upperRaw !== null ? upperRaw : 0]; - } - } else { - point[item.term] = interpolateY(x, item.nodes); - } - }); - dataPoints.push(point); - }); - return dataPoints; - }, [sortedResults]); - - // Tooltip - const renderCustomTooltip = ({ active, payload, label }) => { - if (active && payload && payload.length) { - const dataPoint = payload[0].payload; - - const activeTerms = sortedResults.filter(item => { - if (item.isType2) { - return dataPoint[`${item.term}_upper`] !== null && dataPoint[`${item.term}_upper`] > 0; - } else { - return dataPoint[item.term] !== null && dataPoint[item.term] > 0; - } - }); - - if (activeTerms.length === 0) return null; - - return ( -
-

- Punto X: - {Number(label).toFixed(3)} -

-
- {activeTerms.map(item => { - if (item.isType2) { - const lower = dataPoint[`${item.term}_lower`] !== null ? dataPoint[`${item.term}_lower`] : 0; - const upper = dataPoint[`${item.term}_upper`] !== null ? dataPoint[`${item.term}_upper`] : 0; - const range = Math.abs(upper - lower); - - if (range <= 0.001) { - return ( -
- {item.term} - - Pertenencia: {Number(upper).toFixed(3)} - -
- ); - } - - return ( -
- {item.term} - Mínimo: {Number(lower).toFixed(3)} - Máximo: {Number(upper).toFixed(3)} - - Incertidumbre: {Number(range).toFixed(3)} - -
- ); - } else { - const val = dataPoint[item.term]; - return ( -
- {item.term} - - Pertenencia: {Number(val).toFixed(3)} - -
- ); - } - })} -
-
- ); - } - return null; - }; + useEffect(() => { + const timer = setTimeout(() => { + setIsReady(true); + }, 400); + return () => clearTimeout(timer); + }, []); if (!data || (!data.levels && !data.results)) { - return

Cargando gráfico final...

; + return

Cargando datos...

; } return ( -
- - - -

+
+ + +

{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}

-
- - - - - Number(val.toFixed(2))} - tick={{ fill: '#475569', fontSize: 14 }} - /> - - +
+ + {!isReady && ( +
+
+ Generando gráfica... +
+ )} - {sortedResults.map((item) => { - if (item.isType2) { - return ( + {/* Gráfica */} +
+ {isReady && ( + + + + + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} + /> + + } + cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }} + isAnimationActive={false} + /> + + {sortedResults.map((item) => ( - - - + {item.isType2 ? ( + <> + + + + + ) : ( + + )} - ); - } else { - return ; - } - })} - - + ))} + + + )} +
-
+ {/* Leyenda */} +
{sortedResults.map((item) => ( -
- - {item.term} -
+
+ + {item.term} +
))}
); -}; +}); export default Step3FinalGraph; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/GraphTooltip.jsx b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx new file mode 100644 index 0000000..bc95ac9 --- /dev/null +++ b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx @@ -0,0 +1,53 @@ +const TermInfo = ({ title, color, children }) => ( +
+ {title} + {children} +
+); + +export const GraphTooltip = ({ active, payload, label, sortedResults }) => { + if (!active || !payload || !payload.length) return null; + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => + item.isType2 ? (dataPoint[`${item.term}_upper`] ?? 0) > 0 : (dataPoint[item.term] ?? 0) > 0 + ); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] ?? 0; + const upper = dataPoint[`${item.term}_upper`] ?? 0; + const range = Math.abs(upper - lower); + + return range <= 0.001 ? ( + + Pertenencia: {Number(upper).toFixed(3)} + + ) : ( + + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + + + ); + } + return ( + + Pertenencia: {Number(dataPoint[item.term]).toFixed(3)} + + ); + })} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js new file mode 100644 index 0000000..408cdfc --- /dev/null +++ b/frontend/src/components/editor/finalGraph/useGraphData.js @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import { CHART_COLORS } from '../../../config'; + +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) return nodes[i][1]; + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + if (Math.abs(x2 - x1) < EPSILON) continue; + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + +export const useGraphData = (data) => { + const sortedResults = useMemo(() => { + const rawItems = data?.levels || data?.results || []; + const processed = rawItems.map((item, index) => { + const isType2 = !!item.lower && !!item.upper; + const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; + let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; + + if (isType2) { + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; + } else { + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; + } + }); + return processed.sort((a, b) => a.coreVal - b.coreVal); + }, [data]); + + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + for (let i = 0; i <= steps; i++) xSet.add(Number((i / steps).toFixed(4))); + + sortedResults.forEach(item => { + const addNodes = (nodes) => nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + item.isType2 ? (addNodes(item.lowerNodes), addNodes(item.upperNodes)) : addNodes(item.nodes); + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + return xValues.map(x => { + const point = { x }; + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + point[`${item.term}_range`] = (lowerRaw === null && upperRaw === null) ? null : [lowerRaw ?? 0, upperRaw ?? 0]; + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + return point; + }); + }, [sortedResults]); + + return { sortedResults, denseData }; +}; \ No newline at end of file diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 10529fa..e7067d9 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { getUserHistory, deleteHistoryItem } from '../services/docService'; import Step3FinalGraph from '../components/editor/Step3FinalGraph'; @@ -116,12 +116,21 @@ export default function History() {
- {/* Contenido Desplegable (La gráfica) */} - {isExpanded && ( -
- + {/* Contenido Desplegable (La gráfica)*/} +
+
+ {isExpanded ? ( + + ) : ( +
+ )}
- )} +
+
); })} From 2be291ca130033e40087255d98dc28940bab5d6e Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 10:52:28 +0200 Subject: [PATCH 14/23] =?UTF-8?q?add:=20implementar=20footer=20con=20infor?= =?UTF-8?q?maci=C3=B3n=20del=20proyecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/layout/Footer.jsx | 41 +++++++++++++++++++ frontend/src/components/layout/MainLayout.jsx | 13 +++--- 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/layout/Footer.jsx diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx new file mode 100644 index 0000000..05091c4 --- /dev/null +++ b/frontend/src/components/layout/Footer.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +export default function Footer() { + return ( +
+
+
+ + {/* Información del Proyecto */} +
+ + Deck of Cards + + + Herramienta Científica de Modelado Difuso + +
+ + {/* Enlaces y Redes */} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 9ff0053..60dc8f8 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import Footer from './Footer'; export default function MainLayout({ children }) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -23,9 +24,10 @@ export default function MainLayout({ children }) { }; return ( -
+ // IMPORTANTE: flex y flex-col son la clave para que el footer se quede abajo +
{/* HEADER */} -
+
{/* Logo / Título */} @@ -35,8 +37,6 @@ export default function MainLayout({ children }) { alt="Deck of Cards Logo" className="w-10 h-10 shadow-sm rounded-xl object-contain" /> - - {/* Texto del título */} Deck of Cards @@ -129,9 +129,12 @@ export default function MainLayout({ children }) {
{/* CONTENIDO PRINCIPAL */} -
+
{children}
+ + {/* FOOTER */} +
); } \ No newline at end of file From ed9c608884e9a6e37cfe866bec261ec1945dedba Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 12:46:15 +0200 Subject: [PATCH 15/23] =?UTF-8?q?refactor:=20mejorar=20y=20expandir=20info?= =?UTF-8?q?rmaci=C3=B3n=20de=20footer=20y=20componentizaci=C3=B3n=20de=20h?= =?UTF-8?q?eader.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/uja-logo.png | Bin 0 -> 74499 bytes frontend/src/components/layout/Footer.jsx | 100 ++++++++++--- frontend/src/components/layout/Header.jsx | 80 +++++++++++ frontend/src/components/layout/MainLayout.jsx | 134 +----------------- 4 files changed, 163 insertions(+), 151 deletions(-) create mode 100644 frontend/public/uja-logo.png create mode 100644 frontend/src/components/layout/Header.jsx diff --git a/frontend/public/uja-logo.png b/frontend/public/uja-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..77d17fda40748131e22ddd4eb75ad501fe859837 GIT binary patch literal 74499 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�bS7O`N!80=hY?TB*|QRg;I1TY#Mmv0;~7*g%14_)Sfe?lxV@0A%rkC1mbj;q@a8Io(b?* z6$)UT{MqVI%2p#tz|P+;9-7T%3p)_tV%7$f+1XVa*c3i0?&WvzwAj99{M|^0e zLAPw@eTuk>H?QC0`KiYISib0N@txM&FUq_RW2V+e|AeXaokE{G)_|DJ1yt)jD%dF8 z4#|CBlVcZf3#v0D15fjdsV(E)w@(aDV+nE48cw)kGj`eNRTcdF`71Ry%f`#wrwdJ= zm&r`mFOi0QUmDwev5Yulwkx)ej(hdYuXXxPFi9;F9*~sZyUO*J`D<&PQC|Y|Rq*~2 zf#@N0*RfAWsGsoNzb)FW#0tR{kDF;gL2mU&rlJkBp^yg#t7Nwh&n9N8Kj3}cjJsbM zrXVgUw=VBS1$RkG2;AdWY^yE6m(8+IUo+G(Tlae{;GTZ3drMiaJJzj3&SSZW=-Fr)__rL) z7ENlkOK!!i*8tLzen)MNep8lKa#e5-m|=N9F-KaDOfm2Qdyla@}` z)mi2U!d!Cz!Fl;!oxGDb7`xblmM6#Wy&HQF$5qZuX<@mz`1ZTkH>{|fS5c52-&nu) zcFE$&D&JgN2hSb-MUU7|69Hd!dEr(Y0lYXjNzz^XIs0Y3odyYdaKBU?m#WA8$vWH( z{fzmo!VY|`Moo$YLAy1BbcP+0^xOcbkZa7n` z#vbHttn=Z|MNw^Wes&`&XzVZt|5Ol@G8=2NEceB7dEsAf{$B)lA|MzMBH+4OEH5vV z%i?OZ`hLr9$?f9NQ|TCp#@fJLk5%{&2;{maSzo(0k9L{T!u_707>kcYPDB&G{@_EaAG_FkAkWkn!jn3vXszRp>4|;S zJw=-`wE+p-cA}H#m@bc*ng14|sSyk~`>8WtqQu4}DZv9hP7Ud!OJi>|9@-xm8@e zJHNqu#RzxoAnLq7GgEQtTfS#+T22&r$%k9#qX+KC1-2r;U&GxadC_aje`lI{5^0Xf zRu>UJ<*nj=r;2+%-4wq9{WFmnufcAMFHP^bCvf|X$xPp=;eNxI&S&$@?E@IIuEh5Y z_Yx1x6lY2meSPyygmmC2#iH;YOv2NDzKwa_LHo`hM3L9yZQ)Vhq0Wu9>$BHdXQ!sO zyN)$q6vw%pQNq2AekfBn>v(SJGW22@3u%k#y~XEM$81ZM#EP~Nwm z<R?aSqLU+~b0KveYKV4$Kic)yLeFW>Z_S&BbtU27xG z)=fo?I%-6ubxjvoO(3*?R9BM!37miQK~5e-fE^#^4D;00d`L2 z)#09^fNUz;T;73jArq@7vi*&~y)<*1!n&Z9r|S_khQVNPwtTAy+;6|j%jr0O8^EgQ z&gk{|Hu`GIZ(IACNOpVM#9i;NbsM`T?pJ>{+j~4va<-Pn?WzJkWxhJ?k>5yD*3%-!v zEB_^bhL_Qjn*Z@&@5onWIjQdu&tqYgZmYzF4ynH~*9yUHilAU&tJ~~l#NyM7S6Ip72CJ|A

m#Tr>pH-Tqz?q~gXI{0eGmLFI_qEgd8C6mL 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx index 05091c4..4bb3a2f 100644 --- a/frontend/src/components/layout/Footer.jsx +++ b/frontend/src/components/layout/Footer.jsx @@ -1,40 +1,94 @@ -import React from 'react'; - export default function Footer() { return ( -