Last week, I open-sourced my Advent Calendar project in VueJS. The solution consists of a Calendar view, password protection, “anti-cheat protection” and 24 individual Day views. This week, I wanted to show you how you can simplify the 24 individual views to one DayFactory which is reading the content for each day from a JSON file. This might be useful if all of the days should have the same styling and format but different texts or images, for example. Also, I will show you how to do multiple different calendars from the same code base with just a few modifications!
So grab a coffee and let’s talk about: VueJS (again 😉).
Multiple calendars
Let’s start with the multiple calendar option first! The reason is that we don’t need to remove any of the code you might have already written, so you can see both ways side-by-side and then decide on your own which version you want to use. Maybe, you’ll even find a solution that combines both approaches?
First, we need to tell our vue-router
to use a prefix for the calendar and day views. To do so, we need to define multiple calendar views with a prefix first. It can either be the same component with a URL property or you could use two different Calendar
components.
// Option 1: one component for with a property: const routes = [ { path: '/:version(individual|simplified)', name: 'Calendar', component: Calendar, props: true }, { path: '/login', name: 'Login', component: Login } ] // Option 2: multiple Calendar components: const routes = [ { path: '/individual', name: 'CalendarIndividual', component: CalendarIndividual }, { path: '/simplified', name: 'CalendarSimplified', component: CalendarSimplified }, { path: '/login', name: 'Login', component: Login } ] // either way: add the prefix here for (let i = 1; i <= days.length; i++) { routes.push({ path: '/individual/day/' + i, component: days[i - 1] }) }
Now we need to tell the Calendar to link to different routes depending on the calendar’s version
property:
<template> <!-- ... --> <div> <router-link :to="'/' + version + '/day/' + i"> {{ i }} </router-link> </div> <!-- ... --> </template> <script> export default { name: 'Calendar', props: ['version'], // ... } </script>
We might want to link back to the correct calendar, so we need to outsource the “Back to calendar” link into a separate component.
<slot name="back"> <back-to-calendar :version="version"></back-to-calendar> </slot>
all of <template> <div class="link-back"> <router-link :to="calendarPath">Back to calendar</router-link> </div> </template> <script> export default { 'name': 'back-to-calendar', props: [ 'version' ], computed: { calendarPath: function () { return (this.version) ? `/${this.version}/` : '/login' } } } </script>
But version
is not yet defined! We don’t want to pass the property through from each of our 24 individual day components, to Day, to BackToCalendar. Instead, we set the version as a property in our routing and pass it as a property from Day
to BackToCalendar
:
<script> export default { name: 'Day', props: ['day'], components: { BackToCalendar }, computed: { version: function () { return this.$route.params.version ?? '' }, // ... </script>
Logging in to another calendar
Now that we have two different calendars, we need to make sure that you can only see the one accessible with your credentials! For this, we’re going to update the guard we implemented last time for our router:
router.beforeEach((to, from, next) => { if (to.path.includes('/login')) { next() return; } // protect every route which does not include /day/ var allowedToPassInvidivual = to.path.includes('/individual/') && Cookies.getCookie('REMEMBERME') var allowedToPassSimplified = to.path.includes('/simplified/') && Cookies.getCookie('REMEMBERME2') if (allowedToPassInvidivual || allowedToPassSimplified) { next() } else { next('/login') } })
So now, we have 2 different cookies. That means, during the login process, we need to set a different cookie depending on the calendar we want to access:
<script> // ... checkLogin: function () { // this is not secure (!) & should be handled in the backend if necessary const pw = new jsSHA("SHA-512", "TEXT", { encoding: "UTF8" }); pw.update(this.pw); pw.update(SECRET); const expiryDate = new Date() expiryDate.setMonth(expiryDate.getMonth() + 2) if (pw.getHash("HEX") === PW_INDIVIDUAL) { Cookies.setCookie('REMEMBERME', true, {expires: expiryDate}) this.$router.replace({path: '/individual/'}) } else if(pw.getHash("HEX") === PW_SIMPLIFIED) { Cookies.setCookie('REMEMBERME2', true, {expires: expiryDate}) this.$router.replace({path: '/simplified/'}) } else { this.error = true } } // ... </script>
As you can see, the code is not very pretty, but it serves our purpose of demonstrating how you could extend the codebase. Feel free to make it more extensible/reusable for your purposes.
The DayFactory
Our last task is to read a JSON file which is containing the content for each of the 24 days of a calendar. The structure I chose is:
{ "simplified": { "settings": { "year": 2020 }, "days": [ { "title": "Day 1", "description": "Description 1", "link": "https://moritzwachter.de/", "image": "https://placekitten.com/800/600" }, // ...
The settings
key is optional, but you could use it to add additional configuration parameters to your calendar(s). In the current implementation, I use it to set the year for the specific calendar. I’ve built a little SimpleDaycomponent with a title, description, a blurred image (which slowly gets sharper on click), and a link (when you click the image for a second time). You can check out the component in the repository: LINK
Now we need a DayFactory
. It needs to be aware of the calendar version
and the specific day
which we will both get as a prop from the router. With this information, we can get all the data from our calendars.json, where you can define your content for every calendar you want to create. With some computed properties (for non-Vue developers: think of getXYZ() methods which you can use in your template), we can extract all the information for the specific day and calendar:
<template> <Day :day="formattedDate"> <div slot="page-content"> <SimpleDay :title="title" :description="description" :link="link" :image="image"></SimpleDay> </div> </Day> </template> <script> import Day from '../Day.vue' import SimpleDay from './SimpleDay.vue' import calendars from './calendars.json' export default { name: 'DayFactory', props: ['version', 'day'], components: { SimpleDay, Day }, computed: { calendar: function () { return calendars[this.version] }, year: function () { return this.calendar.settings.year; }, title: function () { return this.calendar.days[this.day].title }, image: function () { return this.calendar.days[this.day].image }, link: function () { return this.calendar.days[this.day].link }, description: function () { return this.calendar.days[this.day].description }, formattedDate: function () { return this.day + '.12.' + this.year } } } </script>
The only thing left is adding our DayFactory
to the routing now:
const routes = [ { path: '/:version(individual|simplified)', name: 'Calendar', component: Calendar, props: true }, { path: '/login', name: 'Login', component: Login }, { path: '/:version/day/:day', name: "DayFactory", component: DayFactory, props: true } ]
Summary
That simple! Now you even have 2 different options on how to create your very own Advent Calendar. Either with individual components or with content extracted from a JSON file with as many different calendars you want to create. 🙂
You can find the branch to this blog post here: https://github.com/moritzwachter/advent-calendar/tree/multiple-calendars