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)

bash
# 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)

text
src/
├── components/
│   ├── TareaList.js
│   ├── TareaForm.js
│   └── TareaItem.js
├── services/
│   └── tareaService.js
├── utils/
│   └── tokenManager.js
├── App.js
└── index.js

馃搫 Paso 3: index.js (1 minuto)

jsx
// 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)

jsx
// 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)

jsx
// 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)

jsx
// 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)

jsx
// 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)

jsx
// 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)

jsx
// 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)

jsx
// 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:

  1. Carga inicial → Token generado autom谩ticamente

  2. Lista tareas → Con botones de acci贸n

  3. Hacer clic "✏️ Editar" → Formulario cambia a modo edici贸n

  4. Modificar datos → Campos prellenados

  5. "馃捑 Guardar Cambios" → API actualiza la tarea

  6. O "❌ Cancelar" → Vuelve a modo creaci贸n

  7. Lista actualizada → Cambios reflejados inmediatamente

Estructura visual:

text
┌─────────────────────────────────────────┬─────────────────────────────────────────┐
│                                         │                                         │
│  馃攽 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)

PasoTiempoDescripci贸n
15 minConfiguraci贸n inicial
22 minEstructura de carpetas
31 minindex.js
42 minApp.js b谩sico
510 minTokenManager
615 minServicio API con token
720 minTareaList con edici贸n
815 minTareaForm dual (crear/editar)
910 minTareaItem con edici贸n
105 minApp.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:

  1. Demostrar el flujo completo: Crear → Editar → Guardar → Ver cambios

  2. Mostrar localStorage para ver token persistente

  3. Usar Network tab para ver headers con token

  4. Simular error 401 quitando token manualmente

  5. Explicar el patr贸n de estado dual en el formulario

  6. Mostrar CSS Grid Inspector para ver la estructura

馃敡 Para probar:

  1. Asegurar API Laravel corriendo:

bash
php artisan serve --port=8000
  1. Ejecutar React:

bash
npm start
  1. Verificar 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

Entradas m谩s populares de este blog

0-Sistema de Tareas

13-CRUD en Laravel para Web y API con Rutas Normales (No Resource)

10-Introducci贸n a Blade en Laravel con un Ejemplo Pr谩ctico