diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f978f1..e7eaa42 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,14 +1,13 @@ // frontend/src/App.tsx -import { ThemeProvider, createTheme, CssBaseline, Box } from '@mui/material'; -import TablaTitulares from './components/TablaTitulares'; // Lo crearemos ahora +import { ThemeProvider, createTheme, CssBaseline, Container } from '@mui/material'; +import Dashboard from './components/Dashboard'; -// Definimos un tema oscuro similar al de la imagen de referencia const darkTheme = createTheme({ palette: { mode: 'dark', primary: { - main: '#3f51b5', + main: '#90caf9', // Un azul más claro para mejor contraste en modo oscuro }, background: { default: '#121212', @@ -20,12 +19,10 @@ const darkTheme = createTheme({ function App() { return ( - {/* Normaliza el CSS para que se vea bien en todos los navegadores */} - -

Titulares Dashboard

- {/* Aquí irán los demás componentes como el formulario de configuración */} - -
+ + + +
); } diff --git a/frontend/src/components/AddTitularModal.tsx b/frontend/src/components/AddTitularModal.tsx new file mode 100644 index 0000000..db04978 --- /dev/null +++ b/frontend/src/components/AddTitularModal.tsx @@ -0,0 +1,60 @@ +// frontend/src/components/AddTitularModal.tsx + +import { useState } from 'react'; +import { Modal, Box, Typography, TextField, Button } from '@mui/material'; + +// Estilo para el modal, centrado en la pantalla +const style = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + boxShadow: 24, + p: 4, +}; + +interface Props { + open: boolean; + onClose: () => void; + onAdd: (texto: string) => void; +} + +const AddTitularModal = ({ open, onClose, onAdd }: Props) => { + const [texto, setTexto] = useState(''); + + const handleSubmit = () => { + if (texto.trim()) { + onAdd(texto.trim()); + setTexto(''); + onClose(); + } + }; + + return ( + + + + Añadir Titular Manual + + setTexto(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} + /> + + + + + + + ); +}; + +export default AddTitularModal; \ No newline at end of file diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx new file mode 100644 index 0000000..3007ee3 --- /dev/null +++ b/frontend/src/components/Dashboard.tsx @@ -0,0 +1,88 @@ +// frontend/src/components/Dashboard.tsx + +import { useEffect, useState } from 'react'; +import { Box, Button, Typography, Stack } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import SyncIcon from '@mui/icons-material/Sync'; +import type { Titular } from '../types'; +import * as api from '../services/apiService'; +import FormularioConfiguracion from './FormularioConfiguracion'; +import TablaTitulares from './TablaTitulares'; +import AddTitularModal from './AddTitularModal'; + +const Dashboard = () => { + const [titulares, setTitulares] = useState([]); + const [modalOpen, setModalOpen] = useState(false); + + const cargarTitulares = async () => { + try { + const data = await api.obtenerTitulares(); + setTitulares(data); + } catch (error) { + console.error("Error al cargar titulares:", error); + } + }; + + useEffect(() => { + cargarTitulares(); + }, []); + + const handleReorder = async (titularesReordenados: Titular[]) => { + setTitulares(titularesReordenados); // Actualización optimista de la UI + const payload = titularesReordenados.map((item, index) => ({ + id: item.id, + nuevoOrden: index + })); + try { + await api.actualizarOrdenTitulares(payload); + } catch (err) { + console.error("Error al reordenar:", err); + cargarTitulares(); // Revertir en caso de error + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) { + try { + await api.eliminarTitular(id); + setTitulares(titulares.filter(t => t.id !== id)); // Actualizar UI + } catch (err) { + console.error("Error al eliminar:", err); + } + } + }; + + const handleAdd = async (texto: string) => { + try { + await api.crearTitularManual(texto); + cargarTitulares(); // Recargar la lista para ver el nuevo titular + } catch (err) { + console.error("Error al añadir titular:", err); + } + }; + + return ( + <> + + + Titulares Dashboard + + + + + + + + + + + setModalOpen(false)} onAdd={handleAdd} /> + + ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/frontend/src/components/FormularioConfiguracion.tsx b/frontend/src/components/FormularioConfiguracion.tsx new file mode 100644 index 0000000..a3a3303 --- /dev/null +++ b/frontend/src/components/FormularioConfiguracion.tsx @@ -0,0 +1,35 @@ +// frontend/src/components/FormularioConfiguracion.tsx + +import { Box, TextField, Button, Paper, Typography } from '@mui/material'; + +const FormularioConfiguracion = () => { + return ( + + + Configuración + + + + + + + + + + ); +}; + +export default FormularioConfiguracion; \ No newline at end of file diff --git a/frontend/src/components/TablaTitulares.tsx b/frontend/src/components/TablaTitulares.tsx index cb2991c..6ef7619 100644 --- a/frontend/src/components/TablaTitulares.tsx +++ b/frontend/src/components/TablaTitulares.tsx @@ -1,19 +1,22 @@ // frontend/src/components/TablaTitulares.tsx -import { useEffect, useState } from 'react'; import { - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; -import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import DragHandleIcon from '@mui/icons-material/DragHandle'; // Importar el ícono +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; - import type { Titular } from '../types'; -import * as api from '../services/apiService'; -// Componente para una fila de tabla "arrastrable" -const SortableRow = ({ titular }: { titular: Titular }) => { +// La prop `onDelete` se añade para comunicar el evento al componente padre +interface SortableRowProps { + titular: Titular; + onDelete: (id: number) => void; +} + +const SortableRow = ({ titular, onDelete }: SortableRowProps) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id }); const style = { @@ -28,15 +31,19 @@ const SortableRow = ({ titular }: { titular: Titular }) => { }; return ( - - ... {/* Handle para arrastrar */} + + {/* El handle de arrastre ahora es un ícono */} + + + {titular.texto} {titular.fuente} - console.log('Eliminar:', titular.id)}> + {/* Usamos un stopPropagation para que el clic no active el arrastre */} + { e.stopPropagation(); onDelete(titular.id); }}> @@ -44,75 +51,58 @@ const SortableRow = ({ titular }: { titular: Titular }) => { ); }; +interface TablaTitularesProps { + titulares: Titular[]; + onReorder: (titulares: Titular[]) => void; + onDelete: (id: number) => void; +} -// Componente principal de la tabla -const TablaTitulares = () => { - const [titulares, setTitulares] = useState([]); +const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps) => { + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // Evita activar el drag con un simple clic - // Sensores para dnd-kit: reaccionar a clics de puntero - const sensors = useSensors(useSensor(PointerSensor)); - - const cargarTitulares = async () => { - try { - const data = await api.obtenerTitulares(); - setTitulares(data); - } catch (error) { - console.error("Error al cargar titulares:", error); - } - }; - - useEffect(() => { - cargarTitulares(); - }, []); - - const handleDragEnd = (event: any) => { + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; - if (active.id !== over.id) { - setTitulares((items) => { - const oldIndex = items.findIndex((item) => item.id === active.id); - const newIndex = items.findIndex((item) => item.id === over.id); - const newArray = arrayMove(items, oldIndex, newIndex); - - // Creamos el payload para la API - const payload = newArray.map((item, index) => ({ - id: item.id, - nuevoOrden: index - })); - - // Llamada a la API en segundo plano - api.actualizarOrdenTitulares(payload).catch(err => { - console.error("Error al reordenar:", err); - // Opcional: revertir el estado si la API falla - }); - - return newArray; - }); + if (over && active.id !== over.id) { + const oldIndex = titulares.findIndex((item) => item.id === active.id); + const newIndex = titulares.findIndex((item) => item.id === over.id); + const newArray = arrayMove(titulares, oldIndex, newIndex); + onReorder(newArray); // Pasamos el nuevo array al padre para que gestione el estado y la llamada a la API } }; + if (titulares.length === 0) { + return ( + + No hay titulares para mostrar. + + ); + } + return ( - - - t.id)} strategy={verticalListSortingStrategy}> - - - - {/* Celda para el drag handle */} - Texto del Titular - Tipo - Fuente - Acciones - - - - {titulares.map((titular) => ( - - ))} - -
-
-
-
+ + + + t.id)} strategy={verticalListSortingStrategy}> + + + + + Texto del Titular + Tipo + Fuente + Acciones + + + + {titulares.map((titular) => ( + + ))} + +
+
+
+
+
); }; diff --git a/frontend/src/services/apiService.ts b/frontend/src/services/apiService.ts index bdd3b57..029d1fc 100644 --- a/frontend/src/services/apiService.ts +++ b/frontend/src/services/apiService.ts @@ -3,14 +3,11 @@ import axios from 'axios'; import type { Titular } from '../types'; -// La URL base de nuestra API. Ajusta el puerto si es diferente. const API_URL = 'https://localhost:5174/api'; const apiClient = axios.create({ baseURL: API_URL, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); export const obtenerTitulares = async (): Promise => { @@ -18,11 +15,14 @@ export const obtenerTitulares = async (): Promise => { return response.data; }; +export const crearTitularManual = async (texto: string): Promise => { + await apiClient.post('/titulares', { texto }); +}; + export const eliminarTitular = async (id: number): Promise => { await apiClient.delete(`/titulares/${id}`); }; -// DTO para el reordenamiento interface ReordenarPayload { id: number; nuevoOrden: number;