Calendario

Vamos a construir un calendario utilizando Vuetify & Firebase, https://medium.com/@jsfanatik/how-to-build-an-event-calendar-with-vue-js-and-firebase-967d734f0ed0

Instalación Vue CLI

Hacer este paso solo si no tienes instalado Vue CLI

npm install @vue/cli -g

Nuevo proyecto de Vue

vue create app-calendario
cd app-calendario

Agregar Vuetify

vue add vuetify

Instalar Firebase

npm i firebase

Tamaño auto text-area

npm i vue-textarea-autosize

Main.js

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import vuetify from "./plugins/vuetify";

import VueTextareaAutosize from "vue-textarea-autosize";
Vue.use(VueTextareaAutosize);

import firebase from "firebase/app";
import "firebase/firestore";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxx",
  authDomain: "xxx",
  databaseURL: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

export const db = firebase.firestore();

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
}).$mount("#app");

App.vue

<template>
  <v-app>
    <v-content>
      <Calendario />
    </v-content>
  </v-app>
</template>

<script>
  import Calendario from "./components/Calendario";

  export default {
    name: "App",
    components: {
      Calendario
    },
    data: () => ({
      //
    })
  };
</script>

Calendario.vue

Comenzaremos a trabajar en el componente del calendario https://vuetifyjs.com/es-MX/components/calendars#events

<template>
  <v-row class="fill-height">
    <v-col>
      <v-sheet height="64">
        <v-toolbar flat color="white">

          <!-- Botón Agregar Evento -->

          <v-btn outlined class="mr-4" @click="setToday">
            Today
          </v-btn>
          <v-btn fab text small @click="prev">
            <v-icon small>mdi-chevron-left</v-icon>
          </v-btn>
          <v-btn fab text small @click="next">
            <v-icon small>mdi-chevron-right</v-icon>
          </v-btn>
          <v-toolbar-title>{{ title }}</v-toolbar-title>
          <v-spacer></v-spacer>
          <v-menu bottom right>
            <template v-slot:activator="{ on }">
              <v-btn
                outlined
                v-on="on"
              >
                <span>{{ typeToLabel[type] }}</span>
                <v-icon right>mdi-menu-down</v-icon>
              </v-btn>
            </template>
            <v-list>
              <v-list-item @click="type = 'day'">
                <v-list-item-title>Day</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = 'week'">
                <v-list-item-title>Week</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = 'month'">
                <v-list-item-title>Month</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = '4day'">
                <v-list-item-title>4 days</v-list-item-title>
              </v-list-item>
            </v-list>
          </v-menu>
        </v-toolbar>
      </v-sheet>
      <v-sheet height="600">
        <v-calendar
          ref="calendar"
          v-model="focus"
          color="primary"
          :events="events"
          :event-color="getEventColor"
          :event-margin-bottom="3"
          :now="today"
          :type="type"
          @click:event="showEvent"
          @click:more="viewDay"
          @click:date="viewDay"
          @change="updateRange"
        ></v-calendar>

        <!-- Agregar Modal Agregar Evento -->

        <v-menu
          v-model="selectedOpen"
          :close-on-content-click="false"
          :activator="selectedElement"
          full-width
          offset-x
        >
          <v-card
            color="grey lighten-4"
            min-width="350px"
            flat
          >
            <!-- Agregar Funcionalidades Editar y Eliminar -->
            <v-toolbar
              :color="selectedEvent.color"
              dark
            >
              <v-btn icon>
                <v-icon>mdi-pencil</v-icon>
              </v-btn>
              <v-toolbar-title v-html="selectedEvent.name"></v-toolbar-title>
              <v-spacer></v-spacer>
              <v-btn icon>
                <v-icon>mdi-heart</v-icon>
              </v-btn>
              <v-btn icon>
                <v-icon>mdi-dots-vertical</v-icon>
              </v-btn>
            </v-toolbar>
            <v-card-text>
              <span v-html="selectedEvent.details"></span>
            </v-card-text>
            <v-card-actions>
              <v-btn
                text
                color="secondary"
                @click="selectedOpen = false"
              >
                Cancel
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-menu>
      </v-sheet>
    </v-col>
  </v-row>
</template>

<script>
  export default {
    data: () => ({
      today: new Date().toISOString().substr(0, 10),
      focus: new Date().toISOString().substr(0, 10),
      type: 'month',
      typeToLabel: {
        month: 'Month',
        week: 'Week',
        day: 'Day',
        '4day': '4 Days',
      },
      start: null,
      end: null,
      selectedEvent: {},
      selectedElement: null,
      selectedOpen: false,
      events: [],
      // Adicionales...
      name: null,
      details: null,
      color: '#1976D2',
      dialog: false,
      currentlyEditing: null
    }),
    computed: {
      title () {
        const { start, end } = this
        if (!start || !end) {
          return ''
        }

        const startMonth = this.monthFormatter(start)
        const endMonth = this.monthFormatter(end)
        const suffixMonth = startMonth === endMonth ? '' : endMonth

        const startYear = start.year
        const endYear = end.year
        const suffixYear = startYear === endYear ? '' : endYear

        const startDay = start.day + this.nth(start.day)
        const endDay = end.day + this.nth(end.day)

        switch (this.type) {
          case 'month':
            return `${startMonth} ${startYear}`
          case 'week':
          case '4day':
            return `${startMonth} ${startDay} ${startYear} - ${suffixMonth} ${endDay} ${suffixYear}`
          case 'day':
            return `${startMonth} ${startDay} ${startYear}`
        }
        return ''
      },
      monthFormatter () {
        return this.$refs.calendar.getFormatter({
          timeZone: 'UTC', month: 'long',
        })
      },
    },
    mounted () {
      this.$refs.calendar.checkChange()
    },
    methods: {
      viewDay ({ date }) {
        this.focus = date
        this.type = 'day'
      },
      getEventColor (event) {
        return event.color
      },
      setToday () {
        this.focus = this.today
      },
      prev () {
        this.$refs.calendar.prev()
      },
      next () {
        this.$refs.calendar.next()
      },
      showEvent ({ nativeEvent, event }) {
        const open = () => {
          this.selectedEvent = event
          this.selectedElement = nativeEvent.target
          setTimeout(() => this.selectedOpen = true, 10)
        }

        if (this.selectedOpen) {
          this.selectedOpen = false
          setTimeout(open, 10)
        } else {
          open()
        }

        nativeEvent.stopPropagation()
      },
      updateRange ({ start, end }) {
        // You could load events from an outside source (like database) now that we have the start and end dates on the calendar
        this.start = start
        this.end = end
      },
      nth (d) {
        return d > 3 && d < 21
          ? 'th'
          : ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'][d % 10]
      },
    },
  }
</script>

Leer Eventos Firebase

created(){
  this.getEvents();
},
async getEvents() {
  try {
    const snapshot = await db.collection("calEvent").get();
    const events = [];
    snapshot.forEach(doc => {
      console.log(doc.data());
      let appData = doc.data();
      appData.id = doc.id;
      events.push(appData);
    });
    this.events = events;
  } catch (error) {
    console.log(error);
  }
},
<!-- Boton -->
<v-btn color="primary" dark class="mr-4" @click="dialog = true">New Event</v-btn>

<!-- Modal -->
<v-dialog v-model="dialog" max-width="500">
  <v-card>
    <v-container>
      <v-form @submit.prevent="addEvent">
        <v-text-field v-model="name" type="text" label="event name (requiered)"></v-text-field>
        <v-text-field v-model="details" type="text" label="details"></v-text-field>
        <v-text-field v-model="start" type="date" label="start"></v-text-field>
        <v-text-field v-model="end" type="date" label="end"></v-text-field>
        <v-text-field v-model="color" type="color" label="color"></v-text-field>
        <v-btn type="submit" color="primay" class="mr-4" @click.stop="dialog = false">Create Event</v-btn>
      </v-form>
    </v-container>
  </v-card>
</v-dialog>
async addEvent(){
  try {
    if(this.name && this.start && this.end){
      await db.collection('calEvent').add({
        name: this.name,
        details: this.details,
        start: this.start,
        end: this.end,
        color: this.color
      })
      this.getEvents();
      this.name = '';
      this.details = '';
      this.start = '';
      this.end = '';
      this.color = '#1976D2';
    }else{
      alert('Campos obligatorios')
    }
  } catch (error) {
    console.log(error);
  }
},

v-calendar en Español

<v-calendar
  :weekdays="[1, 2, 3, 4, 5, 6, 0]"
  locale="es"
  :short-weekdays="false"
></v-calendar>

Editar y Eliminar



 
 
 




 
 
 
 
 
 
 
 
 


 
 
 



<v-card color="grey lighten-4" min-width="350px" flat>
  <v-toolbar :color="selectedEvent.color" dark>
    <v-btn icon @click="deleteEvent(selectedEvent.id)">
      <v-icon>mdi-delete</v-icon>
    </v-btn>
    <v-toolbar-title v-html="selectedEvent.name"></v-toolbar-title>
    <v-spacer></v-spacer>
  </v-toolbar>
  <v-card-text>
    <v-form v-if="currentlyEditing !== selectedEvent.id">
      {{selectedEvent.name}} - {{selectedEvent.details}}
    </v-form>
    <v-form v-else> 
      <v-text-field v-model="selectedEvent.name" type="text" label="name"></v-text-field>
      <textarea-autosize 
        v-model="selectedEvent.details" type="text" style="width: 100%" :min-height="100" placeholder="add note">
      </textarea-autosize>
    </v-form>
  </v-card-text>
  <v-card-actions>
    <v-btn text color="secondary" @click="selectedOpen = false">Close</v-btn>
    <v-btn text v-if="currentlyEditing !== selectedEvent.id" @click.prevent="editEvent(selectedEvent)">Edit</v-btn>
    <v-btn text v-else @click.prevent="updateEvent(selectedEvent)">Save</v-btn>
  </v-card-actions>
</v-card>
editEvent(ev){
  this.currentlyEditing = ev.id;
},
async updateEvent(ev){
  try {
    await db.collection('calEvent').doc(ev.id).update({
      details: ev.details,
      name: ev.name
    })
    this.selectedOpen = false;
    this.currentlyEditing = null;
  } catch (error) {
    console.log(error);
  }
},
async deleteEvent(ev){
  try {
    await db.collection('calEvent').doc(ev.id).delete();
    this.selectedOpen = false;
    this.getEvents();
  } catch (error) {
    console.log(error);
  }
},

Código Final

<template>
  <v-row class="fill-height">
    <v-col>
      <v-sheet height="64">
        <v-toolbar flat color="white">
          <v-btn color="primary" dark class="mr-4" @click="dialog = true">
            Agregar
          </v-btn>
          <v-btn outlined class="mr-4" @click="setToday">
            Hoy
          </v-btn>
          <v-btn fab text small @click="prev">
            <v-icon small>mdi-chevron-left</v-icon>
          </v-btn>
          <v-btn fab text small @click="next">
            <v-icon small>mdi-chevron-right</v-icon>
          </v-btn>
          <v-toolbar-title>{{ title }}</v-toolbar-title>
          <v-spacer></v-spacer>
          <v-menu bottom right>
            <template v-slot:activator="{ on }">
              <v-btn
                outlined
                v-on="on"
              >
                <span>{{ typeToLabel[type] }}</span>
                <v-icon right>mdi-menu-down</v-icon>
              </v-btn>
            </template>
            <v-list>
              <v-list-item @click="type = 'day'">
                <v-list-item-title>Día</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = 'week'">
                <v-list-item-title>Semana</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = 'month'">
                <v-list-item-title>Mes</v-list-item-title>
              </v-list-item>
              <v-list-item @click="type = '4day'">
                <v-list-item-title>4 días</v-list-item-title>
              </v-list-item>
            </v-list>
          </v-menu>
        </v-toolbar>
      </v-sheet>
      <v-sheet height="600">
        
        <v-calendar
          ref="calendar"
          v-model="focus"
          color="primary"
          :events="events"
          :event-color="getEventColor"
          :event-margin-bottom="3"
          :now="today"
          :type="type"
          @click:event="showEvent"
          @click:more="viewDay"
          @click:date="viewDay"
          @change="updateRange"
          :weekdays="[1, 2, 3, 4, 5, 6, 0]"
          locale="es"
          :short-weekdays="false"
        ></v-calendar>

        <!-- Modal Agregar Evento -->
        <v-dialog v-model="dialog">
          <v-card>
            <v-container>
              <v-form @submit.prevent="addEvent">
                <v-text-field 
                  type="text" label="Agregar Nombre" v-model="name">
                </v-text-field>
                <v-text-field 
                  type="text" label="Agregar un Detalle" v-model="details">
                </v-text-field>
                <v-text-field 
                  type="date" label="Inicio del evento" v-model="start">
                </v-text-field>
                <v-text-field 
                  type="date" label="Fin del evento" v-model="end">
                </v-text-field>
                <v-text-field 
                  type="color" label="Color del evento" v-model="color">
                </v-text-field>
                <v-btn type="submit" color="primary" class="mr-4" 
                @click.stop="dialog = false">Agregar</v-btn>
              </v-form>
            </v-container>
          </v-card>
        </v-dialog>


        <v-menu
          v-model="selectedOpen"
          :close-on-content-click="false"
          :activator="selectedElement"
          offset-x
        >
          <v-card
            color="grey lighten-4"
            min-width="350px"
            flat
          >
            <v-toolbar
              :color="selectedEvent.color"
              dark
            >
              <v-btn icon @click="deleteEvent(selectedEvent)">
                <v-icon>mdi-delete</v-icon>
              </v-btn>
              <v-toolbar-title v-html="selectedEvent.name"></v-toolbar-title>
              <v-spacer></v-spacer>
            </v-toolbar>


            <v-card-text>
             
              <v-form v-if="currentlyEditing !== selectedEvent.id">
                {{selectedEvent.name}} - {{selectedEvent.details}}
              </v-form>

              <v-form v-else>

                <v-text-field 
                  type="text" v-model="selectedEvent.name"
                  label="Editar Nombre">
                </v-text-field>

                <textarea-autosize
                  v-model="selectedEvent.details"
                  type="text"
                  style="width: 100%"
                  :min-height="100"
                ></textarea-autosize>

              </v-form>

            </v-card-text>

            
            <v-card-actions>
              <v-btn
                text
                color="secondary"
                @click="selectedOpen = false"
              >
                Cancel
              </v-btn>
              <v-btn text v-if="currentlyEditing !== selectedEvent.id"
              @click.prevent="editEvent(selectedEvent.id)">Editar</v-btn>

              <v-btn text v-else  @click.prevent="updateEvent(selectedEvent)">Guardar Cambios</v-btn>
            </v-card-actions>
          </v-card>
        </v-menu>
      </v-sheet>
    </v-col>
  </v-row>
</template>

<script>
  import {db} from '../main'

  export default {
    data: () => ({
      today: new Date().toISOString().substr(0,10),
      focus: new Date().toISOString().substr(0,10),
      type: 'month',
      typeToLabel: {
        month: 'Mes',
        week: 'Week',
        day: 'Day',
        '4day': '4 Days',
      },
      start: null,
      end: null,
      selectedEvent: {},
      selectedElement: null,
      selectedOpen: false,
      events: [],
      name: null,
      details: null,
      color: '#1976D2',
      dialog: false,
      currentlyEditing: null
    }),
    computed: {
      title () {
        const { start, end } = this
        if (!start || !end) {
          return ''
        }

        const startMonth = this.monthFormatter(start)
        const endMonth = this.monthFormatter(end)
        const suffixMonth = startMonth === endMonth ? '' : endMonth

        const startYear = start.year
        const endYear = end.year
        const suffixYear = startYear === endYear ? '' : endYear

        const startDay = start.day + this.nth(start.day)
        const endDay = end.day + this.nth(end.day)

        switch (this.type) {
          case 'month':
            return `${startMonth} ${startYear}`
          case 'week':
          case '4day':
            return `${startMonth} ${startDay} ${startYear} - ${suffixMonth} ${endDay} ${suffixYear}`
          case 'day':
            return `${startMonth} ${startDay} ${startYear}`
        }
        return ''
      },
      monthFormatter () {
        return this.$refs.calendar.getFormatter({
          timeZone: 'UTC', month: 'long',
        })
      },
    },
    mounted () {
      this.$refs.calendar.checkChange();
    },
    created(){
      this.getEvents();
    },
    methods: {
      async updateEvent(ev){
        try {

          await db.collection('eventos').doc(ev.id).update({
            name: ev.name,
            details: ev.details
          })

          this.selectedOpen = false;
          this.currentlyEditing = null;

          
        } catch (error) {
          console.log(error);
        }
      },
      editEvent(id){
        this.currentlyEditing = id
      },
      async deleteEvent(ev){
        try {

          await db.collection('eventos').doc(ev.id).delete();
          this.selectedOpen = false;
          this.getEvents();
          
        } catch (error) {
          console.log(error);
        }
      },
      async addEvent(){
        try {
          if(this.name && this.start && this.end){

            await db.collection('eventos').add({
              name: this.name,
              details: this.details,
              start: this.start,
              end: this.end,
              color: this.color
            })

            this.getEvents();

            this.name = null;
            this.details = null;
            this.start = null;
            this.end = null;
            this.color = '#1976D2';

          }else{
            console.log('Campos obligatorios');
          }
        } catch (error) {
          console.log(error);
        }
      },
      async getEvents(){
        try {

          const snapshot = await db.collection('eventos').get();
          const events = [];

          snapshot.forEach(doc => {
            // console.log(doc.id);
            let eventoData = doc.data();
            eventoData.id = doc.id;
            events.push(eventoData);
          })

          this.events = events;
          
        } catch (error) {
          console.log(error);
        }
      },
      viewDay ({ date }) {
        this.focus = date
        this.type = 'day'
      },
      getEventColor (event) {
        return event.color
      },
      setToday () {
        this.focus = this.today
      },
      prev () {
        this.$refs.calendar.prev()
      },
      next () {
        this.$refs.calendar.next()
      },
      showEvent ({ nativeEvent, event }) {
        const open = () => {
          this.selectedEvent = event
          this.selectedElement = nativeEvent.target
          setTimeout(() => this.selectedOpen = true, 10)
        }

        if (this.selectedOpen) {
          this.selectedOpen = false
          setTimeout(open, 10)
        } else {
          open()
        }

        nativeEvent.stopPropagation()
      },
      updateRange ({ start, end }) {
        // You could load events from an outside source (like database) now that we have the start and end dates on the calendar
        this.start = start
        this.end = end
      },
      nth (d) {
        return d > 3 && d < 21
          ? 'th'
          : ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'][d % 10]
      },
    },
  }
</script>