How to make a simple calendar using CSS Grid, Vue and moment/date-fns

Viewed 550

I've made a simple calendar using the Vuejs, momentjs and css-grids layout. But the week starts with Sunday (in american style). How can I change the code to make it start with Monday (european style calendar)? I tried to change the column method decrementing it to 2, but only shows properly august and other months started with wrong weekdays. Also I guess I need somehow change the css option grid-template-columns to support Mondays as the first days of week.

Here's the codepen: https://codepen.io/moogeek/pen/oNWyWvM

const calendar = new Vue({
  el: '#app',
  data() {
    return {
      date:moment(),
      days: [],
      monthName: '',
    }
  },
  methods: {
    column(index) {
      if (index == 0) {
        return this.days[0].day() + 1
      }
    },
    isToday(day) {
      return moment().isSame(day, 'day')
    },
    updateMonth(){
      this.monthName = this.date.format("MMMM YYYY")
      let monthDate = this.date.startOf('month');
    
      this.days = [...Array(monthDate.daysInMonth())].map((_, i) => monthDate.clone().add(i, 'day'))
    },
    nextMonth(){
      this.date.add(1,'month')
      this.updateMonth()
    },
    previousMonth(){
      this.date.subtract(1,'month')
      this.updateMonth()
    },
  },
  mounted() {
    this.updateMonth()
  }
})
html {
  overflow:hidden;
}

#app {
    width:100%;
    height:100%;
    font-size: 100%;
    user-select: none;
    overflow:hidden;
}

.page-content {
  overflow: hidden;
}

.calendar-wrap {
  padding-top:4vh;
}

.calendar-header {
    width: 100vw;
    font-size: 170%;
    text-align:center;
    background-color: transparent;
}
.calendar-header .month-name, .calendar-header button{
  display: inline-block;
}

.calendar-header .month-name{
  color:#000;
  font-size:150%;
}
.calendar-header .current-month-value {
  color:#000;
}

.calendar-header .link, .calendar-header .link:hover,.calendar-header .link:active {text-decoration:none;}

.calendar-header .calendar-prev-month-button {
  margin-right: 1vh;
}

.calendar-header .calendar-next-month-button {
  margin-left: 1vh;
}

.calendar-wrapper {
    align-items: center;
    box-sizing: border-box;
    display: flex;
    justify-content: center;
    padding: 0 2em 0 2em;
  }  
  #calendar{
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    max-width: 1024px;
    width: 100%;
    transition: transform .3s ease 0s;
  }

  #calendar > *{
    align-items: center;
    display: flex;
    justify-content: center;
  }
  
  #calendar > *::before {
    content: "";
    display: inline-block;
    height: 0;
    padding-bottom: 100%;
    width: 1px;
  }
  
  #calendar > *.today{
    color: black;
    border: 0.1em solid black;
    border-radius: 100%;
  }

  #calendar > .day {
    border-radius: 100%;
    margin:3px;
  }
<html>
  <head>
      <meta charset="utf-8">
      <title>Calendar</title>


  </head>
 <body>
   <div id="app" class="page-content">
      <div class="calendar-wrap">      
              <section class="calendar-header">
                  <a @click="previousMonth()"class="link icon-only calendar-prev-month-button"><i class="icon icon-prev"></i></a>
                  <a class="current-month-value link">{{ monthName }}</a>
                  <a @click="nextMonth()" class="link icon-only calendar-next-month-button"><i class="icon icon-next"></i></a>
              </section>

              <section id="calendar-wrapper" class="calendar-wrapper skeletons">
                <main id="calendar">
                  
                    <div class="weekday">S</div>
                    <div class="weekday">M</div>
                    <div class="weekday">T</div>
                    <div class="weekday">W</div>
                    <div class="weekday">T</div>
                    <div class="weekday">F</div>
                    <div class="weekday">S</div>
                  
                  <div v-for="(day, index) in days"
                      :data-date="day.format('DD.MM.YYYY')"
                      :style="{ gridColumn: column(index) }" 
                      :class="{ day, today: isToday(day) }">
                    <span>{{ day.format('D') }}</span>
                  </div>
                </main> 
              </section>
    </div>
</div>
</body>
</html>

upd. Ok perhaps I came to a better solution of wrapping weekdays and days in separated containers and adding grid-column:7 property to the first day child. Also changed column function so it doesn't add addiontal offset:

https://codepen.io/moogeek/pen/ExmRzgb

Now onmount the August month is showing correctly, but if I click prev/next months it is somehow cloned out with September's days order... Please could you let me know what am I doing wrong and how do I correct the code so it can show all months properly:

2 Answers

My modern solution with date-fns can be found on github: https://github.com/kissu/so-date-fns-calendar

It is also hosted here: https://so-date-fns-calendar.netlify.app/

And here is the relevant code to achieve all of this

<template>
  <div>
    <div>
      <button @click="substractOneMonth">previous</button>
      <span style="margin-left: 2rem">
        current: {{ format(currentMonth, 'MMM yyyy') }}
      </span>
      <button style="margin-left: 2rem" @click="addOneMonth">next</button>
    </div>
    <div class="week-days">
      <p v-for="weekName in weekNames" :key="weekName">{{ weekName }}</p>
    </div>

    <div class="days">
      <p
        v-for="(day, index) in daysOfCurrentMonth"
        :key="day"
        class="day"
        :style="`grid-column: column(${index}); grid-column-start: ${
          index === 0 ? weekdayOffset : '0' // basically first-child with a param
        }; color: ${isToday(day) ? 'red' : 'black'};`"
      >
        {{ format(day, 'dd') }}
      </p>
    </div>

    <button @click="resetToToday">Go back to today</button>
  </div>
</template>

<script>
import {
  startOfMonth,
  addMonths,
  format,
  subMonths,
  addDays,
  startOfWeek,
  sub,
  add,
  eachDayOfInterval,
  getDay,
  isToday,
} from 'date-fns'
import fr from 'date-fns/locale/fr'

export default {
  data() {
    return {
      currentMonth: startOfMonth(new Date()),
      firstDayOfWeek: startOfWeek(new Date(), {
        locale: fr,
        weekStartsOn: 1, // monday
      }),
    }
  },
  computed: {
    previousMonth() {
      return startOfMonth(subMonths(new Date(this.currentMonth), 1))
    },
    nextMonth() {
      return startOfMonth(addMonths(new Date(this.currentMonth), 1))
    },
    daysOfCurrentMonth() {
      return eachDayOfInterval({
        start: this.currentMonth,
        end: sub(this.nextMonth, { days: 1 }),
      })
    },
    weekNames() {
      return [...Array(7)].map((_, index) =>
        format(addDays(this.firstDayOfWeek, index), 'EEEEEE')
      )
    },
    weekdayOffset() {
      return (getDay(this.currentMonth) + 7) % 7 || 7 // `|| 7` is basically for sunday, edge case
    },
  },
  methods: {
    format,
    isToday,

    substractOneMonth() {
      this.currentMonth = sub(this.currentMonth, { months: 1 })
    },
    addOneMonth() {
      this.currentMonth = add(this.currentMonth, { months: 1 })
    },
    resetToToday() {
      this.currentMonth = startOfMonth(new Date())
    },
  },
}
</script>

<style scoped>
.days,
.week-days {
  display: grid;
  grid-template-columns: repeat(7, 50px);
  grid-column: 7;
}
</style>

I checked 1 year before and 1 year after, everything looks perfectly fine.
Tell me if you need any additional comments or alike!

Btw, my app is a Nuxt one because I just wanted to spin something quickly and that I'm used to it, but the code here is 100% compatible with vanilla Vue.

Ok I've temporarily added an additional check if the first day of current month is Sunday - then grid-column property is not assigned: https://codepen.io/moogeek/pen/JjNBBqo

<div v-for="(day, index) in days"
    :data-date="day.format('DD.MM.YYYY')" 
    :style="{gridColumn:setGridColumn(index)}"
    :class="{ day, today: isToday(day) }">
    <span>{{ day.format('D') }}</span>
</div>
//inside methods
setGridColumn(index){
      return (this.days[0].startOf('month').format('e')!=6) ? this.column(index) : null;
}

But maybe there are another options to achieve this or possibly it leads to other bugs?

Related