Calendario simple en Laravel 9 con Livewire
Tags: laravel, livewire, beginners
Published: 2022-08-21 14:06:36

En este tutorial, veremos como crear un calendario muy sencillo utilizando la ultima version disponible de Laravel y Livewire.

En este tutorial se asumen conocimientos básicos de Laravel.

## Temas a tratar

- Creación de un nuevo proyecto Laravel
- Instalación de paquetes necesarios
- Configuración básica de Vite

## Creación del proyecto e instalación de paquetes necesarios

Primeramente, creamos nuestro nuevo proyecto desde la línea de comandos;

```
# Utilizamos el instalador de laravel
laravel new calendario

# Ingresamos en el directorio del proyecto
cd calendario

# Yo utilizo Valet para los proyectos
valet link
valet secure calendario

# Intalacion del paquete Livewire
composer require livewire/livewire

# Instalacion de los paquetes NPM, 
# primero actualizamos nuestro instalador
npm install -g npm

# Instalo los paquetes de SASS y FontAwesome
# (opcional si no los quieres utilizar)
npm i sass-loader sass webpack
npm i @fortawesome/fontawesome-free

# Tenemos listos los paquetenes que necesitamos
npm install && npm run dev

# Por último creamos el componente Livewire que utilizaremos
php artisan livewire:make Calendario
```

## Configuración de Vite
Con la nueva versión de Laravel, se deja de utilizar Laravel Mix, y se lo cambia por Vite, que introduce muchas mejoras de rendimiento en tiempo de desarrollo, vamos a realizar algunos ajustes en el archivo vite.config.js, situado en la raíz del proyecto.

Vamos a realizar tres cambios en el archivo, en primer lugar, la ubicación de los archivos CSS, ya que utilizaré SASS, por lo que renombraré esta porción de codigo en el archivo;
```
// cambio el nombre del directorio por scss y 
// la extensión del arxhivo.
...
input: ['resources/scss/app.scss', 'resources/js/app.js'],
...
```

En segundo lugar, en la misma sección de plugins, ingresaremos una solución de [Freek van Der Harten](https://freek.dev/2277-using-laravel-vite-to-automatically-refresh-your-browser-when-changing-a-blade-file), que nos permite ver los cambios que realicemos tanto en los archivos css o js en tiempo real.

En tercer lugar, vamos a utilizar otra solución del querido [Freek](https://freek.dev/2276-making-vite-and-valet-play-nice-together), que nos permite resolver el bloqueo que algunos navegadores aplican cuando se invoca a los archivos css o js que se encuentran servidos por Vite.

El archivo de configuración, quedaría de la siguiente forma;
```
<?php
import fs from 'fs';
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { homedir } from 'os';
import { resolve } from 'path';

let host = 'calendario.dev'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/scss/app.scss', 'resources/js/app.js'],
            refresh: true,
        }),
        // Esto hace que al hacer un cambio en el css o lo que sea, 
        // se refresque automágicamente en la vista
        // Freek Laravel
        {
            name: 'blade',
            handleHotUpdate({ file, server }) {
                if (file.endsWith('.blade.php')) {
                    server.ws.send({
                        type: 'full-reload',
                        path: '*',
                    });
                }
            },
        }
    ],
    server: detectServerConfig(host),
});

function detectServerConfig(host) {
    let keyPath = resolve(homedir(), `.config/valet/Certificates/${host}.key`)
    let certificatePath = resolve(homedir(), `.config/valet/Certificates/${host}.crt`)

    if (!fs.existsSync(keyPath)) {
        return {}
    }

    if (!fs.existsSync(certificatePath)) {
        return {}
    }

    return {
        hmr: { host },
        host,
        https: {
            key: fs.readFileSync(keyPath),
            cert: fs.readFileSync(certificatePath),
        },
    }
}
```

## ¡Manos a la obra!, la vista.
No nos vamos a complicar con crear nuevas vistas, utilizaremos la misma vista de bienvenida que viene por defecto de Laravel;
```
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Calendario</title>

        @vite(['resources/scss/app.scss', 'resources/js/app.js'])

        @livewireStyles

    </head>
    <body>

        <livewire:calendario />

        @livewireScripts
    </body>
</html>
```
Como puede verse, es una muy simple implementación, para mayor detalles sobre cómo integrar un módulo Livewire en una vista, pueden echarle un ojo a [este artículo](https://laravel-livewire.com/docs/2.x/installation#include-js) en la documentación oficial.

## Vista de Livewire
En el archivo resources/views/livewire/calendario.blade.php deben implementar lo siguiente;
```
<div>
    {{-- The Master doesn't talk, he acts. --}}
    <div class="calendar">
        <div></div>
      <div class="cabecera">
        <i class="fas fa-angle-left nav" wire:click="decrementar('m')"></i>
        <div>{{ $meses[$countMes] }}</div>
        <i class="fas fa-angle-right nav" wire:click="incrementar('m')"></i>
        
        <i class="fas fa-angle-left nav" wire:click="decrementar('a')"></i>
        <div><span class="year">{{ $anio }}</span></div>
        <i class="fas fa-angle-right nav " wire:click="incrementar('a')"></i>
      </div>
      
      <div class="days">
        @foreach ($etiquetaDias as $dia)
        <span>{{ $dia }}</span>
        @endforeach
      </div>
        
      <div class="dates">
        @php
          $extraClass = "";
        @endphp
        
        @while ($inicioCalendario <= $finCalendario)
  
          @php
            $extraClass = $inicioCalendario->format('m') != $fecha->format('m') ? 'deshabilitado' : '';
            $extraClass .= $inicioCalendario->isToday() ? ' today ' : '';
          @endphp
  
          <button  class="{{ $extraClass }}" >
            <time>{{ $inicioCalendario->format('j') }}</time>
          </button>
          
          @php
            $inicioCalendario->addDay();
          @endphp 
        @endwhile
      </div>
    </div>
  </div>
```
Esto es básicamente la forma en que se arma el calendario, nos valdremos de Carbon para armar un calendario de la forma mas sencilla posible, el artículo de donde obtengo esta porción de código es el [siguiente](https://jonathanbriehl.com/posts/build-a-simple-calendar-with-carbon-and-laravel).

## El Componente, o Controlador del calendario
Con Livewire, no llevamos la lógica en un controlador de la clase Controller como es lo común en Laravel, si no que se crea un nuevo componente en el directorio app/Http/Livewire/Calendario.php, en donde escribiremos lo siguiente;
```
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Illuminate\Support\Carbon;

class Calendario extends Component
{
    protected $currentDateTime;
    
    public $inicioCalendario;
    public $finCalendario;
    public $fecha;
    public $anio;
    public $countMes = 1;
    public $etiquetaDias = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
    public $meses = [ 1 => 'Enero',
                      2 => 'Febrero',
                      3 => 'Marzo',
                      4 => 'Abril',
                      5 => 'Mayo',
                      6 => 'Junio',
                      7 => 'Julio',
                      8 => 'Agosto',
                      9 => 'Septiembre',
                      10 => 'Octubre',
                      11 => 'Noviembre',
                      12 => 'Diciembre', ];

    // inicializamos calendario
    public function mount()
    {
        $this->currentDateTime = now();
        
        $this->countMes = $this->currentDateTime->format('n');
        $this->inicioCalendario = $this->currentDateTime->copy()->firstOfMonth()->startOfWeek(Carbon::SUNDAY);
        $this->finCalendario = $this->currentDateTime->copy()->lastOfMonth()->endOfWeek(Carbon::SATURDAY);
        $this->fecha = $this->currentDateTime->copy();
        $this->anio = $this->currentDateTime->copy()->format('Y');
    }

    /**
     * Metodo para incrementar, tanto por mes, como por año
     */
    public function incrementar($objeto)
    {
        // Creamos una instacia de la fecha con el mes, y año que está en la vista
        $this->currentDateTime = Carbon::createFromFormat('n/Y', $this->countMes.'/'.$this->anio);

        // Si el parametro es M es porque se incrementa un mes,
        // por lo que nos valemos del metodo addMonth de Carbon
        if ($objeto == "m") {
            $this->inicioCalendario = $this->currentDateTime->copy()
                                            ->addMonth()
                                            ->firstOfMonth()
                                            ->startOfWeek(Carbon::SUNDAY);
            $this->finCalendario = $this->currentDateTime->copy()
                                        ->addMonth()
                                        ->lastOfMonth()
                                        ->endOfWeek(Carbon::SATURDAY);
            
            // establecemos los valores para mes y año que figuran en la vista
            $this->countMes = (int) $this->currentDateTime->copy()->addMonth()->format('n');
            $this->anio = (int) $this->currentDateTime->copy()->addMonth()->format('Y');
        } else {
            // Si el parametro es A es porque se incrementa un año,
            // por lo que nos valemos del metodo addYear de Carbon
            $this->inicioCalendario = $this->currentDateTime->copy()
                                            ->addYear()
                                            ->firstOfMonth()
                                            ->startOfWeek(Carbon::SUNDAY);
            $this->finCalendario = $this->currentDateTime->copy()
                                        ->addYear()
                                        ->lastOfMonth()
                                        ->endOfWeek(Carbon::SATURDAY);
            // establecemos los valores para mes y año que figuran en la vista
            $this->countMes = (int) $this->currentDateTime->copy()->addYear()->format('n');
            $this->anio = (int) $this->currentDateTime->copy()->addYear()->format('Y');
        }
    }

    /**
     * Metodo para decrementar, tanto por mes, como por año
     * La lógica es la misma que para el incremento,
     * pero utilizando los metodos subMonth y subYear de Carbon
     */
    public function decrementar($objeto)
    {
        $this->currentDateTime = Carbon::createFromFormat('n/Y', $this->countMes.'/'.$this->anio);

        if ($objeto == "m") {
            $this->inicioCalendario = $this->currentDateTime->copy()
                                            ->subMonth()
                                            ->firstOfMonth()
                                            ->startOfWeek(Carbon::SUNDAY);
            $this->finCalendario = $this->currentDateTime->copy()
                                        ->subMonth()
                                        ->lastOfMonth()
                                        ->endOfWeek(Carbon::SATURDAY);
            
            $this->countMes = (int) $this->currentDateTime->copy()->subMonth()->format('n');
            $this->anio = (int) $this->currentDateTime->copy()->subMonth()->format('Y');
        } else {
            $this->inicioCalendario = $this->currentDateTime->copy()
                                            ->subYear()
                                            ->firstOfMonth()
                                            ->startOfWeek(Carbon::SUNDAY);
            $this->finCalendario = $this->currentDateTime->copy()
                                        ->subYear()
                                        ->lastOfMonth()
                                    ->endOfWeek(Carbon::SATURDAY);
            
            $this->countMes = (int) $this->currentDateTime->copy()->subYear()->format('n');
            $this->anio = (int) $this->currentDateTime->copy()->subYear()->format('Y');
        }
    }
    
    /**
     * Renderizado de la vista
     */
    public function render()
    {
        return view('livewire.calendario');
    }
}
```
El código está lo suficientemente comentado, por lo que no deberían tener muchos problemas para entenderlo.

## Formato
Por último, en el archivo resources/scss/app.scss vamos a definir los estilos para el calendario;
```
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "@fortawesome/fontawesome-free/scss/solid.scss";
@import "@fortawesome/fontawesome-free/scss/brands.scss";
@import "@fortawesome/fontawesome-free/scss/regular.scss";
body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.calendar {
    display: inline-grid;
    justify-content: center;
    align-items: center;
    background: #fff;
    padding: 20px;
    .cabecera {
        display: flex;
        justify-content: space-between;
        align-items: center;
        font-size: 20px;
        margin-bottom: 20px;
        font-weight: 300;
        .year {
            font-weight: 600;
            margin-left: 10px;
        }
        .nav {
            display: flex;
            justify-content: center;
            align-items: center;
            text-decoration: none;
            color: #0a3d62;
            width: 40px;
            height: 40px;
            border-radius: 10px;
            transition-duration: 0.2s;
            position: relative;
            &:hover {
                background: #eee;
            }
        }
    }
    .days {
        display: grid;
        justify-content: center;
        align-items: center;
        grid-template-columns: repeat(7, 1fr);
        color: #999;
        font-weight: 600;
        margin-bottom: 15px;
        span {
            width: 50px;
            justify-self: center;
            align-self: center;
            text-align: center;
        }
    }
    .dates {
        display: grid;
        grid-template-columns: repeat(7, 1fr);
        button {
            cursor: pointer;
            outline: 0;
            border: 0;
            background: transparent;
            font-family: "Montserrat", sans-serif;
            font-size: 16px;
            justify-self: center;
            align-self: center;
            width: 50px;
            height: 50px;
            border-radius: 10px;
            margin: 2px;
            transition-duration: 0.2s;
            &.today {
                box-shadow: inset 0px 0px 0px 2px #0a3d62;
            }
            &.ocupado {
                background: #0a3d62;
                color: #fff;
                font-weight: 600;
                &:hover {
                    background: #a9a;
                }
            }
            &.deshabilitado {
                color: #aaa;
                font-weight: 600;
            }
            &:hover {
                background: #eee;
            }
        }
    }
}
```

## El resultado
Con la utilizacion de Vite, a medida que escriben podrán ir viendo los resultados sin necesidad de recargar la pagina, de todas les dejo el calendario terminado;

![Calendario terminado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykbmpjyx47vz80e0cipu.png)

## ¿Y ahora?
Ahora resta aplicarle mas complejidad, como una conexion a la base de datos para poder registrar eventos para una aplicacion de agenda, por ejemplo, les dejo el [código fuente](https://github.com/javier-superfunk/calendario) del proyecto para que descarguen y despejen cualquier duda.