Skip to content

LaureanoC/React-Django

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Table of Contents generated with DocToc

DJANGO Y REACT

Creación del entorno virtual

pip install virtualenv

comando + nombre de carpeta

python -m venv venv

Activamos el entorno virtual para luego ejecutar los comandos de django.

Django

Para inicializar un proyecto en django lo primero que tenemos que hacer es ejecutar el siguiente comando, el punto es para que no cree una carpeta.

django-admin startproject django_crud_api .

Luego para ejecutarlo, en la termina vamos hacia la carpeta que tiene el manage.py

python manage.py runserver

Configurando Django

Creando una app

python manage.py startapp nombreapp

Agregando la app a django

Para ello vamos a ir a las settings.py en django_crud_api y agregamos en INSTALLED_APPS el parametro de como se llama la app, en nuestro caso task.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'task'
]

Luego realizaremos una migración.

If your database doesn't exist yet, migrate creates all the necessary tables to match your model definitions.

Otherwise if the database already exists, migrate updates the existing table definitions to match the model definitions -- i.e. maybe you added a field to one of your models, so migrate would add that column to the database table.

python manage.py migrate

Django REST Framework

Sitio alternativo pero con documentación directa a REST

https://www.django-rest-framework.org

Instalamos el modulo

pip install djangorestframework

Middleware CORS - django-cors-headers

Para conectar los dos servidores de desarrollo necesitamos habilitar el CORS.

pip install django-cors-headers

Para configurarlo vamos a incluir en installed aps -> rest_framework y corsheaders añadimos.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'rest_framework',
    'task'
]

Luego en MIDDLEWARE debemos añadir el cors pero no en cualquier lado sino que deberia estar antes del django.middleware.common.CommonMiddleware

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Luego configuremos cual servidor puede conectarse a django. Agregaremos al final

CORS_ALLOWED_ORIGINS = [
    'poner la url aqui'
]

Utilizando el ORM

Crearemos unos modelos para que el orm lo utilice. Para ello vamos a task y luego a models.py

from django.db import models

# Create your models here.

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    done = models.BooleanField(default=False)

Para crear la tabla

Para crear todas las tablas

python manage.py makemigrations

Para crear una en especifico

python manage.py makemigrations task

Al ejecutar esto se creara una carpeta llamada migrations donde habrá un archivo que se encontrara el codigo que ejecutara python para crear nuestra tabla. Aún asi aun no se ejecutó.

Para ello ejecutaremos el siguiente comando

python manage.py migrate nombredeapp

Perfecto.

Ahora iremos al panel de administrador de django localhost:8000/admin

Pero antes tendremos que crear una cuenta.

python manage.py createsuperuser

Ahora ya podemos entrar pero no vamos a ver nuestras tablas por lo que tenemos que agregarlas en admin.py

from django.contrib import admin

# Register your models here.

from .models import Task
admin.site.register(Task)

Y listo ya podemos utilzar el crud que viene por defecto en django.

Aún asi al registrar una tarea al leerla nos aparece Task Object. Debemos configurar eso.

Vamos a model y utilizamos def str(self):

def __str__(self):
        return self.title

Creación de API

Primero le diremos al modelo de tarea que datos serán seleccionados para que se puedan enviar desde el backend y puedan ser convertidos en json.

Para ello crearemos un archivo serializer.py

from rest_framework import serializers

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ('id','title','description', 'done')

De esta forma le decimos que datos queremos que los convierta en JSON.

Y si son muuuchos campos

from rest_framework import serializers

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        fields = '__all__'

Le agreraremos un "nombre" al modelo.

from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ('id','title','description', 'done')

Proseguiremos a crear vistas. Las vistas son funciones que le responden algo al cliente, en nuestro caso un CRUD. Al ser una tarea repetitiva, django nos provee una funcionalidad de crear todo el crud de forma automatica.

serializer_class basicamente tiene la clase que ya habia creado, para darle informacion que campos seran consultados importamos el modelo de tarea.

from rest_framework import viewsets
from .serializer import TaskSerializer
from .models import Task

# Create your views here.}

class TaskView(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    queryset = Task.objects.all()

Ahora seguiremos con las URL. Crearemos un archivo urls.py

El código de abajo genera las rutas GET, POST, PUT y DELETE

from django.urls import path, include
from rest_framework import routers
from task import views

router = routers.DefaultRouter()
router.register(r'tasks', views.TaskView, 'tasks')

urlpatterns = [
    path('api/v1', include(router.urls))
]

Ahora debemos hacer que la aplicación principal (django) conozca tambien las url.

vamos a su urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('tasks/', include('task.urls'))
]

Accedemos http://127.0.0.1:8000/tasks/api/v1/tasks/

y si queremos por tarea

http://127.0.0.1:8000/tasks/api/v1/tasks/1/

Podemos hacer el CRUD también ahi con su interfaz.

Añadir módulo docs automático

Este módulo documenta.

pip install coreapi

Vamos a configurarlo. En INSTALLED_APPS, encima de tasks, insertamos el modulo coreapi.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'rest_framework',
    'coreapi',
    'task'
]

En tasks urls añadimos las siguientes lineas. /docs con su importación.

from django.urls import path, include
from rest_framework.documentation import include_docs_urls
from rest_framework import routers
from task import views

router = routers.DefaultRouter()
router.register(r'tasks', views.TaskView, 'tasks')

urlpatterns = [
    path('api/v1/', include(router.urls)),
    path('docs/', include_docs_urls(title= 'Task API'))
]

Por ultimo añadiremos esto en settings.py

REST_FRAMEWORK = {
    ...: ...,
    "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
}

Al ejecutar a mi me salio un error, me faltaba un paquete, setuptools

pip install setuptools

React

Utilizaremos una herramienta moderna como vitesjs para poner React en marcha.

Creamos una nueva terminal.

npm create vite

Seguimos los pasos. Y cuando termine

Done. Now run:

cd client

npm install

npm run dev

Comunicación entre servidores

Instalaremos los siguientes modulos.

react-router-dom (para tener multiples paginas en el front) react-hot-toast (obtener mensajitos si eliminamos algo por ejemplo) axios (para hacer peticiones algo como fetch pero mas sencillo) react-hook-form (validar inputs del frontend)

npm i react-router-dom react-hot-toast axios react-hook-form

Limpiaremos un poco el proyecto.

src>App.jsx

Eliminamos casi todo el contenido excepto la función App() y su export.

function App() {
  return <div>Hello world!</div>
}

export default App

Eliminamos contenido de index.css

Creamos las carpetas pages, otra components, api, todo dentro de src.

Pages para nuestras páginas. Components para almacenar fracciones de interfaz. Api para ddefinir que funciones pediran datos al backend.

Dentro de pages creamos dos archivos TaskPage.jsx para listar todas las tareas y TaskFormPage para crear un formulario de tareas.

En TaskPage crearemos un componente basico.

export function TasksPage() {
  return <div>TaskPage</div>
}

Y en TaskFormPage lo mismo.

export function TaskFormPage() {
  return <div>TaskFormPage</div>
}

Ahora llamaremos a esas páginas, para ello vamos a App.jsx

path es el nombre de la ruta. element es el componente que renderizaremos.

import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { TasksPage } from './pages/TaskPage'
import { TaskFormPage } from './pages/TaskFormPage'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/tasks' element={<TasksPage />} />
        <Route path='/tasks-create' element={<TaskFormPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App

Y lo podemos comprobar accediendo a la url.

Ahora setearemos la página de inicio. Podríamos hacer lo mismo pero también podemos decirle que redireccione con el componente Navigate.

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { TasksPage } from './pages/TaskPage'
import { TaskFormPage } from './pages/TaskFormPage'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/' element={<Navigate to='/tasks' />} />
        <Route path='/tasks' element={<TasksPage />} />
        <Route path='/tasks-create' element={<TaskFormPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App

Ahora crearemos una navegación bien sencilla dentro de components.

import { Link } from 'react-router-dom'

export function Navigation() {
  return (
    <div>
      <Link to='/tasks'>
        <h1>Task App</h1>
      </Link>
      <Link to='/tasks-create'>Create task</Link>
    </div>
  )
}

Añadimos la navegación en App.jsx

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { TasksPage } from './pages/TaskPage'
import { TaskFormPage } from './pages/TaskFormPage'
import { Navigation } from './components/Navigation'

function App() {
  return (
    <BrowserRouter>
      <Navigation />

      <Routes>
        <Route path='/' element={<Navigate to='/tasks' />} />
        <Route path='/tasks' element={<TasksPage />} />
        <Route path='/tasks-create' element={<TaskFormPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App

Crearemos un nuevo componente llamado TaskList.jsx que nos listara cada tarea.

export function TaskList() {
  return <div>TaskList</div>
}

Ahora queremos que cada vez que sea llamado ese componente se ejecute un comportamiento. Para ello importamos useEffect y que cuando se cargue realizar un simple console.log()

import { useEffect } from 'react'

export function TaskList() {
  useEffect(() => {
    console.log('Página cargada')
  }, [])

  return <div>TaskList</div>
}

Ahora debemos llamar al componente, lo llamaremos en TaskPage.jsx

import { TaskList } from '../components/TaskList'

export function TasksPage() {
  return <TaskList />
}

Ahora vamos a la consola y vemos que 2 mensajes aparecieron y no 1. Ello se debe a que en el modo desarrollo existe un Componente en React llamado React.StrictMode que es el culpable. Se encuentra en main.jsx.

Crearemos un archivo llamado tasks.api.js en api. Utilizaremos axios.

import axios from 'axios'

export const getAllTasks = () => {
  return axios.get('http://localhost:8000/tasks/api/v1/tasks/')
}

Vamos a importar la funcion en TaskList para que haga la petición.

import { useEffect } from 'react'
import { getAllTasks } from '../api/tasks.api'

export function TaskList() {
  useEffect(() => {
    async function loadTasks() {
      const res = await getAllTasks()
      console.log(res)
    }

    loadTasks()
  }, [])

  return <div>TaskList</div>
}

Nos saldrá error pues el backend no habilitó la entrada de esta petición.+

Vamos a django y en settings.py

CORS_ALLOWED_ORIGINS = ['http://localhost:5173']

Para guardar los datos en React no se utilizan variables sino que se usa la función useState.

import { useEffect, useState } from 'react'
import { getAllTasks } from '../api/tasks.api'

export function TaskList() {
  const [tasks, setTasks] = useState()

  useEffect(() => {
    async function loadTasks() {
      const res = await getAllTasks()
      setTasks(res.data)
    }

    loadTasks()
  }, [])

  return <div>TaskList</div>
}

Ahora vamos a listarla en html, ver que cambie en useState y le agregué []

import { useEffect, useState } from 'react'
import { getAllTasks } from '../api/tasks.api'

export function TaskList() {
  const [tasks, setTasks] = useState([])

  useEffect(() => {
    async function loadTasks() {
      const res = await getAllTasks()
      setTasks(res.data)
    }

    loadTasks()
  }, [])

  return (
    <div>
      {tasks.map((tasks) => (
        <div key={tasks.id}>
          <h1>{tasks.title}</h1>
          <p>{tasks.description}</p>
        </div>
      ))}
    </div>
  )
}

Vamos a ordenar, ahora crearemos un componente para renderizar las tareas.

export function TaskCard({ task }) {
  return (
    <div key={tasks.id}>
      <h1>{tasks.title}</h1>
      <p>{tasks.description}</p>
    </div>
  )
}

Y en listcard

import { useEffect, useState } from 'react'
import { getAllTasks } from '../api/tasks.api'
import { TaskCard } from './TaskCard'

export function TaskList() {
  const [tasks, setTasks] = useState([])

  useEffect(() => {
    async function loadTasks() {
      const res = await getAllTasks()
      setTasks(res.data)
    }

    loadTasks()
  }, [])

  return (
    <div>
      {tasks.map((task) => (
        <TaskCard key={task.id} task={task} />
      ))}
    </div>
  )
}

Taskcard

export function TaskCard({ task }) {
  return (
    <div>
      <h1>{task.title}</h1>
      <p>{task.description}</p>
      <hr />
    </div>
  )
}

Crear

Iremos a TaskFormPage y haremos un formulario.

export function TaskFormPage() {
  return (
    <div>
      <form action=''>
        <input type='text' placeholder='Title' />
        <textarea rows='3' placeholder='Description'></textarea>
        <button>Save</button>
      </form>
    </div>
  )
}

Utilizaremos la librería react-hook-form para validar los datos.

import { useForm } from 'react-hook-form'

export function TaskFormPage() {
  const { register } = useForm()

  return (
    <div>
      <form action=''>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        <button>Save</button>
      </form>
    </div>
  )
}

Manejaremos el botón de save con errores.

import { useForm } from 'react-hook-form'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()

  const onSubmit = handleSubmit((data) => {
    console.log(data)
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>
    </div>
  )
}

Crearemos el servicio de createTask, para ello configuraremos antes axios para que tenga una urlbase y asi no tener que repetir.

import axios from 'axios'

const taskApi = axios.create({
  baseURL: 'http://localhost:8000/tasks/api/v1/tasks/',
})

export const getAllTasks = () => taskApi.get('/')
export const createTask = (task) => taskApi.post('/', task)

Ahora vamos al handleSubmit para llamar al servicio.

const onSubmit = handleSubmit(async (data) => {
  const res = await createTask(data)
  console.log(res)
})

Por ultimo haremos que se redireccione al inicio para ello utilizaremos react-router-dom, con useNavigate

import { useForm } from 'react-hook-form'
import { createTask } from '../api/tasks.api'
import { useNavigate } from 'react-router-dom'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()
  const navigate = useNavigate()

  const onSubmit = handleSubmit(async (data) => {
    await createTask(data)
    navigate('/')
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>
    </div>
  )
}

Editar y Eliminar

Modificaremos el TaskCard para dejarlo listo para cuadno pongamos una ruta.

import { useNavigate } from 'react-router-dom'
export function TaskCard({ task }) {
  const navigate = useNavigate()
  return (
    <div onClick={() => {}}>
      <h1>{task.title}</h1>
      <p>{task.description}</p>
      <hr />
    </div>
  )
}

Vamos a crear la ruta. Recordemos que se encuentra en App.jsx

<Routes>
  <Route path='/' element={<Navigate to='/tasks' />} />
  <Route path='/tasks' element={<TasksPage />} />
  <Route path='/tasks-create' element={<TaskFormPage />} />
  <Route path='/tasks/:id' element={<TaskFormPage />} />
</Routes>

Vamos a hacer la tarjeta navegable.

import { useNavigate } from 'react-router-dom'
export function TaskCard({ task }) {
  const navigate = useNavigate()
  return (
    <div
      onClick={() => {
        navigate(`/tasks/${task.id}`)
      }}
    >
      <h1>{task.title}</h1>
      <p>{task.description}</p>
      <hr />
    </div>
  )
}

Y ahora agregaremos el botón de eliminar con un if comparando el parametro de la url para eso utilizaremos un hook llamado useParams

export function TaskFormPage() {
  const { register, handleSubmit, formState: {errors} } = useForm()
	const navigate = useNavigate()
	const params = useParams()

  const onSubmit = handleSubmit(async data => {
    await createTask(data)
		navigate('/')
  })

  return ( ........

Agregamos el if, con console log podemos ver que trae params. ejemplo { id: '5'}, si está vacío no trae nada, solo {}

{
  params.id && <button>Delete</button>
}

Creamos el servicio. No nos olvidemos que debe terminar con /

export const deleteTask = (id) => taskApi.delete(`/${id}/`)

Manejamos el click.

import { useForm } from 'react-hook-form'
import { createTask, deleteTask } from '../api/tasks.api'
import { useNavigate, useParams } from 'react-router-dom'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()
  const navigate = useNavigate()
  const params = useParams()

  const onSubmit = handleSubmit(async (data) => {
    await createTask(data)
    navigate('/')
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>

      {params.id && (
        <button
          onClick={async () => {
            const accepted = window.confirm('are you sure')
            if (accepted) {
              await deleteTask(params.id)
              navigate('/')
            }
          }}
        >
          Delete
        </button>
      )}
    </div>
  )
}

Ahora proseguiremos con editar. Pondremos un if al igual que antes para ver si se está actualizando o creando.

const onSubmit = handleSubmit(async (data) => {
  if (params.id) {
    console.log('actualizando')
  } else {
    await createTask(data)
  }
  navigate('/')
})

Crearemos el servicio.

import axios from 'axios'

const taskApi = axios.create({
  baseURL: 'http://localhost:8000/tasks/api/v1/tasks/',
})

export const getAllTasks = () => taskApi.get('/')
export const createTask = (task) => taskApi.post('/', task)
export const deleteTask = (id) => taskApi.delete(`/${id}/`)
export const updateTask = (id, task) => taskApi.put(`/${id}/`, task)

Y lo ponenmos dentro del if

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()
  const navigate = useNavigate()
  const params = useParams()

  const onSubmit = handleSubmit(async (data) => {
    if (params.id) {
      console.log("actualizando")
    } else {
      await createTask(data)
    }
    navigate("/")
  })

Haremos como que obtenemos los datos...

import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { createTask, deleteTask } from '../api/tasks.api'
import { useNavigate, useParams } from 'react-router-dom'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()
  const navigate = useNavigate()
  const params = useParams()

  const onSubmit = handleSubmit(async (data) => {
    if (params.id) {
      console.log('actualizando')
    } else {
      await createTask(data)
    }
    navigate('/')
  })

  useEffect(() => {
    if (params.id) {
      console.log('obteniendo datos')
    }
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>

      {params.id && (
        <button
          onClick={async () => {
            const accepted = window.confirm('are you sure')
            if (accepted) {
              await deleteTask(params.id)
              navigate('/')
            }
          }}
        >
          Delete
        </button>
      )}
    </div>
  )
}

Crearemos el servicio de getOne.

export const getTask = (id) => taskApi.get(`/${id}/`)

Haremos una función asincrona dentro del useEffect.

useEffect(() => {
  async function loadTask() {
    if (params.id) {
      await getTask(params.id)
    }
  }

  loadTask()
})

Rellenamos los inputs. desde useForm utilizamos setValue('')

	useEffect(()=>{
		async function loadTask() {
			if (params.id){
				const res = await getTask(params.id)
				setValue('title', res.data.title)
				setValue('description', res.data.description)
			}
		}

Quedando todo TaskFormPage así:

import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { createTask, deleteTask, getTask, updateTask } from '../api/tasks.api'
import { useNavigate, useParams } from 'react-router-dom'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setValue,
  } = useForm()
  const navigate = useNavigate()
  const params = useParams()

  const onSubmit = handleSubmit(async (data) => {
    if (params.id) {
      await updateTask(params.id, data)
    } else {
      await createTask(data)
    }
    navigate('/')
  })

  useEffect(() => {
    async function loadTask() {
      if (params.id) {
        const res = await getTask(params.id)
        setValue('title', res.data.title)
        setValue('description', res.data.description)
      }
    }

    loadTask()
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>

      {params.id && (
        <button
          onClick={async () => {
            const accepted = window.confirm('are you sure')
            if (accepted) {
              await deleteTask(params.id)
              navigate('/')
            }
          }}
        >
          Delete
        </button>
      )}
    </div>
  )
}

React-hot-toast (mensajes)

Importamos en app.jsx react-hot-toast y lo colocamos luego de las rutas. El componente siempre estará disponible solo hay que visualizarlo.

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { TasksPage } from './pages/TaskPage'
import { TaskFormPage } from './pages/TaskFormPage'
import { Navigation } from './components/Navigation'
import { Toaster } from 'react-hot-toast'

function App() {
  return (
    <BrowserRouter>
      <Navigation />

      <Routes>
        <Route path='/' element={<Navigate to='/tasks' />} />
        <Route path='/tasks' element={<TasksPage />} />
        <Route path='/tasks-create' element={<TaskFormPage />} />
        <Route path='/tasks/:id' element={<TaskFormPage />} />
      </Routes>
      <Toaster />
    </BrowserRouter>
  )
}

export default App

Para ello importaremos toast en el lugar que queremos.

const onSubmit = handleSubmit(async (data) => {
  if (params.id) {
    await updateTask(params.id, data)
  } else {
    await createTask(data)
    toast.success('Task created')
  }
  navigate('/')
})

Si queremos lo podemos personalizar.

toast.success('Task created', {
  position: 'bottom-right',
  style: {
    background: '#101010',
    color:'#fff'
  }
})

Aaí con el resto

  const onSubmit = handleSubmit(async (data) => {
    if (params.id) {
      await updateTask(params.id, data)
      toast.success('Task updated', {
        position: 'bottom-right',
        style: {
          background: '#101010',
          color: '#fff',
        },
      })
    } else {
      await createTask(data)
      toast.success('Task created', {
        position: 'bottom-right',
        style: {
          background: '#101010',
          color: '#fff',
        },
      })
    }
    navigate('/')
  })

Delete

<button
          onClick={async () => {
            const accepted = window.confirm('are you sure')
            if (accepted) {
              await deleteTask(params.id)
              toast.success('Task deleted', {
                position: 'bottom-right',
                style: {
                  background: '#101010',
                  color: '#fff',
                },
              })
              navigate('/')
            }
          }}
        >
          Delete
        </button>

Quedando así todoo.

import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { createTask, deleteTask, getTask, updateTask } from '../api/tasks.api'
import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-hot-toast'

export function TaskFormPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setValue,
  } = useForm()
  const navigate = useNavigate()
  const params = useParams()

  const onSubmit = handleSubmit(async (data) => {
    if (params.id) {
      await updateTask(params.id, data)
      toast.success('Task updated', {
        position: 'bottom-right',
        style: {
          background: '#101010',
          color: '#fff',
        },
      })
    } else {
      await createTask(data)
      toast.success('Task created', {
        position: 'bottom-right',
        style: {
          background: '#101010',
          color: '#fff',
        },
      })
    }
    navigate('/')
  })

  useEffect(() => {
    async function loadTask() {
      if (params.id) {
        const res = await getTask(params.id)
        setValue('title', res.data.title)
        setValue('description', res.data.description)
      }
    }

    loadTask()
  })

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type='text'
          placeholder='Title'
          {...register('title', { required: true })}
        />
        {errors.title && <span>this field is required</span>}
        <textarea
          rows='3'
          placeholder='Description'
          {...register('description', { required: true })}
        ></textarea>
        {errors.description && <span>this field is required</span>}
        <button>Save</button>
      </form>

      {params.id && (
        <button
          onClick={async () => {
            const accepted = window.confirm('are you sure')
            if (accepted) {
              await deleteTask(params.id)
              toast.success('Task deleted', {
                position: 'bottom-right',
                style: {
                  background: '#101010',
                  color: '#fff',
                },
              })
              navigate('/')
            }
          }}
        >
          Delete
        </button>
      )}
    </div>
  )
}

Tailwind

Instalamos tailwind, obviamente dentro de client.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Editaremos tailwind config para que se vea asi

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

En index.css importamos base, components y utilities.

@tailwind base;
@tailwind components;
@tailwind utilities;

Y empezamos a personalizar, recordar que en React es className.