17-Tutorial Paso a Paso - Gestor de Tareas con API, Token y Edici贸n
Tutorial Paso a Paso - Gestor de Tareas con API, Token y Edici贸n
馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)
# 1.1 Crear proyecto React
npx create-react-app gestor-tareas-token-edicion
cd gestor-tareas-token-edicion
# 1.2 Eliminar archivos innecesarios
rm src/App.test.js src/logo.svg src/reportWebVitals.js src/setupTests.js
# 1.3 Iniciar el proyecto
npm start馃搧 Paso 2: Estructura de Carpetas (2 minutos)
src/
├── components/
│ ├── TareaList.js
│ ├── TareaForm.js
│ └── TareaItem.js
├── services/
│ └── tareaService.js
├── utils/
│ └── tokenManager.js
├── App.js
└── index.js馃搫 Paso 3: index.js (1 minuto)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);馃摝 Paso 4: App.js inicial (2 minutos)
// src/App.js
import React from 'react';
import TareaList from './components/TareaList';
function App() {
return (
<div>
<h1>馃摑 Gestor de Tareas (Token + Edici贸n)</h1>
<p>API Laravel protegida con token y edici贸n completa</p>
<TareaList />
</div>
);
}
export default App;馃攽 Paso 5: Gestor de Token (10 minutos)
// src/utils/tokenManager.js
class TokenManager {
constructor() {
this.tokenKey = 'api_token';
}
// Obtener token del localStorage
getToken() {
return localStorage.getItem(this.tokenKey);
}
// Guardar token en localStorage
saveToken(token) {
localStorage.setItem(this.tokenKey, token);
}
// Eliminar token
clearToken() {
localStorage.removeItem(this.tokenKey);
}
// Verificar si hay token
hasToken() {
return !!this.getToken();
}
// Generar nuevo token desde la API
async generarNuevoToken() {
try {
const response = await fetch('http://localhost:8000/api/token/generar', {
method: 'POST'
});
if (!response.ok) {
throw new Error('Error al generar token');
}
const data = await response.json();
// Guardar el token
this.saveToken(data.token);
return {
success: true,
token: data.token,
message: 'Token generado y guardado'
};
} catch (error) {
console.error('Error:', error);
return {
success: false,
message: error.message
};
}
}
// Forzar generaci贸n de token si no existe
async asegurarToken() {
if (!this.hasToken()) {
console.log('No hay token, generando uno nuevo...');
return await this.generarNuevoToken();
}
return { success: true, token: this.getToken() };
}
}
// Crear una instancia 煤nica
const tokenManager = new TokenManager();
export default tokenManager;馃攲 Paso 6: Servicio de API con Token (15 minutos)
// src/services/tareaService.js
import tokenManager from '../utils/tokenManager';
const API_URL = 'http://localhost:8000/api';
const tareaService = {
// Configuraci贸n com煤n para todas las peticiones
async configurarPeticion(metodo, datos = null) {
// Asegurar que tenemos un token
const tokenResult = await tokenManager.asegurarToken();
if (!tokenResult.success) {
throw new Error('No se pudo obtener token: ' + tokenResult.message);
}
const token = tokenResult.token;
// Configurar headers
const config = {
method: metodo,
headers: {
'Content-Type': 'application/json',
'Token': token // Enviar token en el header
}
};
// Agregar body si hay datos
if (datos) {
config.body = JSON.stringify(datos);
}
return config;
},
// 1. Obtener todas las tareas
async obtenerTodas() {
try {
const config = await this.configurarPeticion('GET');
const respuesta = await fetch(`${API_URL}/tareas`, config);
if (respuesta.status === 401) {
// Token inv谩lido, generar uno nuevo y reintentar
await tokenManager.generarNuevoToken();
return await this.obtenerTodas(); // Reintentar
}
if (!respuesta.ok) {
throw new Error('Error al obtener tareas');
}
const data = await respuesta.json();
return data.tareas || data;
} catch (error) {
console.error('Error:', error);
throw error;
}
},
// 2. Obtener una tarea espec铆fica (para editar)
async obtenerPorId(id) {
try {
const config = await this.configurarPeticion('GET');
const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
if (respuesta.status === 401) {
await tokenManager.generarNuevoToken();
return await this.obtenerPorId(id);
}
if (!respuesta.ok) {
throw new Error('Error al obtener tarea');
}
const data = await respuesta.json();
return data.tarea || data;
} catch (error) {
console.error('Error:', error);
throw error;
}
},
// 3. Crear nueva tarea
async crearTarea(tarea) {
try {
const config = await this.configurarPeticion('POST', tarea);
const respuesta = await fetch(`${API_URL}/tareas`, config);
if (respuesta.status === 401) {
await tokenManager.generarNuevoToken();
return await this.crearTarea(tarea);
}
if (!respuesta.ok) {
throw new Error('Error al crear tarea');
}
return await respuesta.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
},
// 4. Actualizar tarea (para editar y completar)
async actualizarTarea(id, datos) {
try {
const config = await this.configurarPeticion('PUT', datos);
const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
if (respuesta.status === 401) {
await tokenManager.generarNuevoToken();
return await this.actualizarTarea(id, datos);
}
if (!respuesta.ok) {
throw new Error('Error al actualizar tarea');
}
return await respuesta.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
},
// 5. Eliminar tarea
async eliminarTarea(id) {
try {
const config = await this.configurarPeticion('DELETE');
const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
if (respuesta.status === 401) {
await tokenManager.generarNuevoToken();
return await this.eliminarTarea(id);
}
if (!respuesta.ok) {
throw new Error('Error al eliminar tarea');
}
return await respuesta.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
},
// 6. Obtener token actual
getTokenActual() {
return tokenManager.getToken();
},
// 7. Generar nuevo token manualmente
async generarNuevoTokenManual() {
return await tokenManager.generarNuevoToken();
},
// 8. Limpiar token
limpiarToken() {
tokenManager.clearToken();
}
};
export default tareaService;馃搵 Paso 7: TareaList.js con Token y Edici贸n (20 minutos)
// src/components/TareaList.js
import React, { useState, useEffect } from 'react';
import TareaForm from './TareaForm';
import TareaItem from './TareaItem';
import tareaService from '../services/tareaService';
function TareaList() {
// Estados principales
const [tareas, setTareas] = useState([]);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState('');
const [tokenInfo, setTokenInfo] = useState('');
// Estados para edici贸n
const [editando, setEditando] = useState(null); // Tarea que se est谩 editando
const [modoFormulario, setModoFormulario] = useState('crear'); // 'crear' o 'editar'
// CSS Grid para estructura
const estiloContenedor = {
display: 'grid',
gridTemplateColumns: '350px 1fr',
gap: '20px',
padding: '20px'
};
const estiloColumna = {
border: '2px solid #333',
padding: '15px',
borderRadius: '8px'
};
const estiloTokenPanel = {
background: '#e3f2fd',
padding: '12px',
marginBottom: '15px',
borderRadius: '6px',
border: '1px solid #bbdefb'
};
const estiloModoPanel = {
background: editando ? '#fff3e0' : '#e8f5e9',
padding: '10px',
marginBottom: '15px',
borderRadius: '6px',
border: `1px solid ${editando ? '#ffcc80' : '#c8e6c9'}`,
textAlign: 'center'
};
// Cargar tareas al iniciar
useEffect(() => {
cargarTareas();
actualizarTokenInfo();
}, []);
// Mostrar informaci贸n del token
const actualizarTokenInfo = () => {
const token = tareaService.getTokenActual();
setTokenInfo(token ? `Token: ${token}` : 'No hay token');
};
// Funci贸n para cargar tareas
const cargarTareas = async () => {
try {
setCargando(true);
setError('');
const datos = await tareaService.obtenerTodas();
setTareas(datos);
actualizarTokenInfo();
} catch (error) {
setError(`❌ Error: ${error.message}`);
console.error('Error detallado:', error);
} finally {
setCargando(false);
}
};
// Funci贸n para crear tarea
const manejarCrearTarea = async (nuevaTarea) => {
try {
await tareaService.crearTarea(nuevaTarea);
cargarTareas();
setModoFormulario('crear');
} catch (error) {
alert('Error al crear la tarea: ' + error.message);
}
};
// Funci贸n para actualizar tarea
const manejarActualizarTarea = async (id, datosActualizados) => {
try {
await tareaService.actualizarTarea(id, datosActualizados);
setEditando(null);
setModoFormulario('crear');
cargarTareas();
} catch (error) {
alert('Error al actualizar la tarea: ' + error.message);
}
};
// Funci贸n para eliminar tarea
const manejarEliminarTarea = async (id) => {
if (window.confirm('¿Eliminar esta tarea?')) {
try {
await tareaService.eliminarTarea(id);
// Si estamos editando esta tarea, cancelar edici贸n
if (editando && editando.id === id) {
setEditando(null);
setModoFormulario('crear');
}
cargarTareas();
} catch (error) {
alert('Error al eliminar la tarea: ' + error.message);
}
}
};
// Funci贸n para completar/descompletar tarea
const manejarCompletarTarea = async (id, completada) => {
try {
await tareaService.actualizarTarea(id, { completada });
cargarTareas();
} catch (error) {
alert('Error al actualizar la tarea: ' + error.message);
}
};
// Funci贸n para iniciar edici贸n
const iniciarEdicion = async (tarea) => {
try {
// Obtener la tarea completa desde la API
const tareaCompleta = await tareaService.obtenerPorId(tarea.id);
setEditando(tareaCompleta);
setModoFormulario('editar');
} catch (error) {
alert('Error al cargar tarea para editar: ' + error.message);
}
};
// Funci贸n para cancelar edici贸n
const cancelarEdicion = () => {
setEditando(null);
setModoFormulario('crear');
};
// Funci贸n para generar nuevo token
const generarNuevoToken = async () => {
try {
const resultado = await tareaService.generarNuevoTokenManual();
if (resultado.success) {
alert(`✅ Nuevo token generado: ${resultado.token}`);
actualizarTokenInfo();
cargarTareas();
} else {
alert(`❌ Error: ${resultado.message}`);
}
} catch (error) {
alert('Error al generar token: ' + error.message);
}
};
return (
<div style={estiloContenedor}>
{/* COLUMNA IZQUIERDA: Formulario y token */}
<div style={estiloColumna}>
<div style={estiloTokenPanel}>
<strong>馃攽 Autenticaci贸n por Token</strong>
<div style={{ marginTop: '5px', fontSize: '14px' }}>
{tokenInfo}
</div>
<button
onClick={generarNuevoToken}
style={{
marginTop: '10px',
padding: '5px 10px',
fontSize: '12px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '3px'
}}
>
馃攧 Generar Nuevo Token
</button>
</div>
<div style={estiloModoPanel}>
<strong>
{modoFormulario === 'editar' ? '✏️ EDITANDO TAREA' : '➕ NUEVA TAREA'}
</strong>
{editando && (
<div style={{ fontSize: '12px', marginTop: '5px' }}>
Editando: {editando.titulo}
</div>
)}
</div>
<TareaForm
onSubmit={
modoFormulario === 'editar'
? (datos) => manejarActualizarTarea(editando.id, datos)
: manejarCrearTarea
}
tareaInicial={editando}
onCancelar={modoFormulario === 'editar' ? cancelarEdicion : null}
modo={modoFormulario}
/>
<div style={{ marginTop: '20px' }}>
<button
onClick={cargarTareas}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px'
}}
>
馃攧 Recargar Tareas
</button>
</div>
</div>
{/* COLUMNA DERECHA: Lista de tareas */}
<div style={estiloColumna}>
<h2>馃搵 Lista de Tareas</h2>
{error && (
<div style={{
background: '#ffebee',
color: '#c62828',
padding: '10px',
marginBottom: '15px',
borderRadius: '5px'
}}>
{error}
</div>
)}
{cargando ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<p>Cargando tareas desde API...</p>
<p style={{ fontSize: '12px', color: '#666' }}>
(Se est谩 generando/verificando el token autom谩ticamente)
</p>
</div>
) : tareas.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<p>馃摥 No hay tareas</p>
<p>Crea tu primera tarea usando el formulario</p>
</div>
) : (
<>
<div style={{
background: '#f5f5f5',
padding: '10px',
marginBottom: '15px',
borderRadius: '5px'
}}>
<strong>✅ Conectado a la API</strong>
<div style={{ fontSize: '14px', marginTop: '5px' }}>
Total: {tareas.length} tareas | Token activo
</div>
</div>
{tareas.map((tarea) => (
<TareaItem
key={tarea.id}
tarea={tarea}
onEditar={iniciarEdicion}
onEliminar={manejarEliminarTarea}
onCompletar={manejarCompletarTarea}
estaEditando={editando && editando.id === tarea.id}
/>
))}
</>
)}
</div>
</div>
);
}
export default TareaList;✏️ Paso 8: TareaForm.js con Modo Dual (15 minutos)
// src/components/TareaForm.js
import React, { useState, useEffect } from 'react';
function TareaForm({ onSubmit, tareaInicial = null, onCancelar, modo = 'crear' }) {
// Estado del formulario
const [formData, setFormData] = useState({
titulo: '',
descripcion: '',
completada: false
});
const [error, setError] = useState('');
const [enviando, setEnviando] = useState(false);
// Cargar datos cuando cambia tareaInicial (modo edici贸n)
useEffect(() => {
if (tareaInicial) {
setFormData({
titulo: tareaInicial.titulo || '',
descripcion: tareaInicial.descripcion || '',
completada: tareaInicial.completada || false
});
} else {
// Resetear formulario para modo creaci贸n
setFormData({
titulo: '',
descripcion: '',
completada: false
});
}
}, [tareaInicial]);
// Manejar cambios en los inputs
const manejarCambio = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
// Limpiar error si el usuario empieza a escribir
if (error && name === 'titulo' && value.trim()) {
setError('');
}
};
// Manejar env铆o del formulario
const manejarSubmit = async (e) => {
e.preventDefault();
if (!formData.titulo.trim()) {
setError('El t铆tulo es obligatorio');
return;
}
setEnviando(true);
try {
await onSubmit(formData);
// Solo resetear si es modo creaci贸n
if (modo === 'crear') {
setFormData({ titulo: '', descripcion: '', completada: false });
}
setError('');
} catch (error) {
alert('Error al guardar: ' + error.message);
} finally {
setEnviando(false);
}
};
return (
<form onSubmit={manejarSubmit}>
{error && (
<div style={{
background: '#ffebee',
color: '#c62828',
padding: '8px',
marginBottom: '15px',
borderRadius: '4px'
}}>
{error}
</div>
)}
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
T铆tulo:
</label>
<input
type="text"
name="titulo"
value={formData.titulo}
onChange={manejarCambio}
placeholder={modo === 'editar' ? "Editar t铆tulo..." : "Ej: Comprar leche"}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
disabled={enviando}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Descripci贸n:
</label>
<textarea
name="descripcion"
value={formData.descripcion}
onChange={manejarCambio}
placeholder={modo === 'editar' ? "Editar descripci贸n..." : "Ej: Ir al supermercado"}
rows="4"
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
fontFamily: 'inherit'
}}
disabled={enviando}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="checkbox"
name="completada"
checked={formData.completada}
onChange={manejarCambio}
style={{
marginRight: '10px',
width: '18px',
height: '18px'
}}
disabled={enviando}
/>
<span style={{ fontSize: '16px' }}>
{modo === 'editar' ? '¿Tarea completada?' : 'Marcar como completada'}
</span>
</label>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: onCancelar ? '1fr 1fr' : '1fr',
gap: '10px'
}}>
<button
type="submit"
disabled={enviando}
style={{
padding: '12px',
backgroundColor: modo === 'editar' ? '#2196F3' : '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '16px',
fontWeight: 'bold',
cursor: enviando ? 'not-allowed' : 'pointer',
opacity: enviando ? 0.7 : 1
}}
>
{enviando ? '⏳ Guardando...' : (
modo === 'editar' ? '馃捑 Guardar Cambios' : '➕ Crear Tarea'
)}
</button>
{onCancelar && (
<button
type="button"
onClick={onCancelar}
disabled={enviando}
style={{
padding: '12px',
backgroundColor: '#9E9E9E',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '16px',
cursor: enviando ? 'not-allowed' : 'pointer'
}}
>
❌ Cancelar
</button>
)}
</div>
</form>
);
}
export default TareaForm;馃摝 Paso 9: TareaItem.js con Bot贸n de Edici贸n (10 minutos)
// src/components/TareaItem.js
import React from 'react';
function TareaItem({ tarea, onEditar, onEliminar, onCompletar, estaEditando }) {
// Estilos din谩micos basados en el estado
const estiloTarea = {
border: '1px solid #ddd',
padding: '15px',
marginBottom: '10px',
borderRadius: '8px',
backgroundColor: tarea.completada ? '#e8f5e9' : '#ffffff',
borderLeft: estaEditando ? '5px solid #2196F3' :
tarea.completada ? '5px solid #4CAF50' : '5px solid #FFC107',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
};
const estiloBotones = {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)', // 3 columnas iguales
gap: '10px',
marginTop: '15px'
};
const estiloEstado = {
display: 'inline-block',
padding: '3px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold',
backgroundColor: tarea.completada ? '#4CAF50' : '#FFC107',
color: tarea.completada ? 'white' : '#333',
marginLeft: '10px'
};
return (
<div style={estiloTarea}>
{/* Header con t铆tulo y estado */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h3 style={{
marginTop: 0,
marginBottom: '5px',
color: tarea.completada ? '#2E7D32' : '#333'
}}>
{tarea.titulo}
<span style={estiloEstado}>
{tarea.completada ? 'COMPLETADA' : 'PENDIENTE'}
</span>
</h3>
</div>
{estaEditando && (
<span style={{
backgroundColor: '#2196F3',
color: 'white',
padding: '3px 10px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold'
}}>
✏️ EDITANDO
</span>
)}
</div>
{/* Descripci贸n */}
<p style={{
color: '#666',
margin: '10px 0',
lineHeight: '1.5'
}}>
{tarea.descripcion || <em style={{ color: '#999' }}>Sin descripci贸n</em>}
</p>
{/* Metadatos */}
<div style={{
color: '#888',
fontSize: '13px',
marginBottom: '10px',
display: 'flex',
gap: '15px'
}}>
<span>ID: <strong>{tarea.id}</strong></span>
{tarea.created_at && (
<span>Creada: {new Date(tarea.created_at).toLocaleDateString()}</span>
)}
<span>馃攼 Token protegido</span>
</div>
{/* Botones de acci贸n */}
<div style={estiloBotones}>
<button
onClick={() => onCompletar(tarea.id, !tarea.completada)}
style={{
padding: '8px',
backgroundColor: tarea.completada ? '#FF9800' : '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
title={tarea.completada ? 'Marcar como pendiente' : 'Marcar como completada'}
>
{tarea.completada ? '↩️ Desmarcar' : '✅ Completar'}
</button>
<button
onClick={() => onEditar(tarea)}
style={{
padding: '8px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
title="Editar esta tarea"
disabled={estaEditando}
>
✏️ Editar
</button>
<button
onClick={() => onEliminar(tarea.id)}
style={{
padding: '8px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
title="Eliminar esta tarea"
>
馃棏️ Eliminar
</button>
</div>
</div>
);
}
export default TareaItem;馃幆 Paso 10: App.js mejorado (5 minutos)
// src/App.js
import React from 'react';
import TareaList from './components/TareaList';
const estiloApp = {
padding: '20px',
maxWidth: '1300px',
margin: '0 auto',
minHeight: '100vh'
};
const estiloHeader = {
textAlign: 'center',
marginBottom: '30px',
paddingBottom: '20px',
borderBottom: '3px solid #eee'
};
const estiloFeatures = {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
marginTop: '20px',
marginBottom: '30px'
};
const estiloFeature = {
background: '#f5f5f5',
padding: '15px',
borderRadius: '8px',
textAlign: 'center',
border: '1px solid #e0e0e0'
};
function App() {
return (
<div style={estiloApp}>
<header style={estiloHeader}>
<h1 style={{
color: '#333',
marginBottom: '10px',
fontSize: '32px'
}}>
馃攼 Gestor de Tareas Completo
</h1>
<p style={{
color: '#666',
fontSize: '18px',
marginBottom: '20px'
}}>
API Laravel con autenticaci贸n por token + Edici贸n completa
</p>
<div style={estiloFeatures}>
<div style={estiloFeature}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>馃攼</div>
<strong>Autenticaci贸n por Token</strong>
<p style={{ fontSize: '14px', marginTop: '5px' }}>Token autom谩tico y seguro</p>
</div>
<div style={estiloFeature}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>✏️</div>
<strong>Edici贸n Completa</strong>
<p style={{ fontSize: '14px', marginTop: '5px' }}>Modificar cualquier tarea</p>
</div>
<div style={estiloFeature}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>馃摫</div>
<strong>Dise帽o Responsivo</strong>
<p style={{ fontSize: '14px', marginTop: '5px' }}>CSS Grid + Layout moderno</p>
</div>
</div>
</header>
<main>
<TareaList />
</main>
<footer style={{
marginTop: '40px',
textAlign: 'center',
color: '#888',
fontSize: '14px',
paddingTop: '20px',
borderTop: '1px solid #eee'
}}>
<p>馃捇 Tutorial React + Laravel API | Token + CRUD Completo</p>
</footer>
</div>
);
}
export default App;✅ Verificaci贸n Final
Flujo completo de edici贸n:
Carga inicial → Token generado autom谩ticamente
Lista tareas → Con botones de acci贸n
Hacer clic "✏️ Editar" → Formulario cambia a modo edici贸n
Modificar datos → Campos prellenados
"馃捑 Guardar Cambios" → API actualiza la tarea
O "❌ Cancelar" → Vuelve a modo creaci贸n
Lista actualizada → Cambios reflejados inmediatamente
Estructura visual:
┌─────────────────────────────────────────┬─────────────────────────────────────────┐
│ │ │
│ 馃攽 AUTENTICACI脫N POR TOKEN │ 馃搵 LISTA DE TAREAS │
│ Token: 123456 │ │
│ [馃攧 Generar Nuevo Token] │ ✅ Conectado a la API │
│ │ Total: X tareas | Token activo │
│ ✏️ EDITANDO TAREA │ │
│ Editando: Comprar leche │ • ✅ Comprar leche │
│ │ Ir al supermercado │
│ [T铆tulo] _______________ │ ID: 1 | Creada: 2024-01-17 │
│ │ COMPLETADA ✏️ EDITANDO │
│ [Descripci贸n] _________ │ 馃攼 Token protegido │
│ ______ │ [↩️] [✏️ Disabled] [馃棏️] │
│ │ │
│ [✓] ¿Tarea completada? │ • ⏳ Estudiar React │
│ │ Hacer ejercicios │
│ [馃捑 Guardar Cambios] [❌ Cancelar] │ ID: 2 | Creada: 2024-01-16 │
│ │ PENDIENTE │
│ [馃攧 Recargar Tareas] │ 馃攼 Token protegido │
│ │ [✅] [✏️ Editar] [馃棏️] │
└─────────────────────────────────────────┴─────────────────────────────────────────┘馃搳 Resumen de Tiempo (85 minutos total)
| Paso | Tiempo | Descripci贸n |
|---|---|---|
| 1 | 5 min | Configuraci贸n inicial |
| 2 | 2 min | Estructura de carpetas |
| 3 | 1 min | index.js |
| 4 | 2 min | App.js b谩sico |
| 5 | 10 min | TokenManager |
| 6 | 15 min | Servicio API con token |
| 7 | 20 min | TareaList con edici贸n |
| 8 | 15 min | TareaForm dual (crear/editar) |
| 9 | 10 min | TareaItem con edici贸n |
| 10 | 5 min | App.js mejorado |
馃幆 Caracter铆sticas implementadas:
1. Sistema de Token:
✅ Generaci贸n autom谩tica
✅ Persistencia en localStorage
✅ Reintento autom谩tico si token inv谩lido
✅ Panel visual del token actual
✅ Bot贸n para regenerar token
2. Funcionalidad de Edici贸n:
✅ Modo dual en formulario (crear/editar)
✅ Carga de datos al editar
✅ Bot贸n "Editar" en cada tarea
✅ Indicador visual de edici贸n activa
✅ Botones "Guardar Cambios" y "Cancelar"
3. Interfaz de Usuario:
✅ CSS Grid de 2 columnas
✅ Feedback visual claro
✅ Estados deshabilitados durante operaciones
✅ Dise帽o responsivo
✅ Colores indicativos de estado
4. Manejo de Errores:
✅ Errores de token manejados autom谩ticamente
✅ Validaci贸n de formulario
✅ Mensajes de error amigables
✅ Estados de carga claros
馃挕 Tips para la explicaci贸n:
Demostrar el flujo completo: Crear → Editar → Guardar → Ver cambios
Mostrar localStorage para ver token persistente
Usar Network tab para ver headers con token
Simular error 401 quitando token manualmente
Explicar el patr贸n de estado dual en el formulario
Mostrar CSS Grid Inspector para ver la estructura
馃敡 Para probar:
Asegurar API Laravel corriendo:
php artisan serve --port=8000Ejecutar React:
npm startVerificar en navegador (localhost:3000):
Token se genera autom谩ticamente
Puedes crear, editar, eliminar y marcar tareas
Todo est谩 protegido por token
¡Perfecto! El alumno ahora tiene una aplicaci贸n React completa con autenticaci贸n por token y funcionalidad de edici贸n completa, ideal para mostrar un CRUD profesional con seguridad
Comentarios
Publicar un comentario