Posts Module Year/Month Archive List
Created 7 years ago by bloveless

It took me a while to figure all this out, so I'm putting it here so other people can benefit from this! Here is how I created a year/month archive lists (with counts of the number of posts in each year and month) in the posts module using the entries plugin.

{% set years = entries('posts').selectRaw('DISTINCT YEAR(`publish_at`) as year').orderByRaw('YEAR(`publish_at`)').get() %}
{% for year in years %}
    {% set yearCount = entries('posts').whereRaw('YEAR(`publish_at`) = ' ~ year.year).count() %}
    <div class="archive-year">
        <a href="/posts/archive/{{ year.year }}">{{ year.year }} ({{ yearCount }})</a>
    </div>
    {% set curYearMonths = entries('posts').selectRaw('DISTINCT MONTHNAME(`publish_at`) as month_name, MONTH(`publish_at`) as month').orderByRaw('MONTH(`publish_at`)').whereRaw('YEAR(`publish_at`) = ' ~ year.year).get() %}
    {% for month in curYearMonths %}
        {% set monthCount = entries('posts').whereRaw('YEAR(`publish_at`) = ' ~ year.year ~ ' and MONTH(`publish_at`) = ' ~ month.month).count() %}
        <div class="archive-month">
            <a href="/posts/archive/{{ year.year }}/{{ month.month }}">{{ month.month_name }} ({{ monthCount }})</a>
        </div>
    {% endfor %}
{% endfor %}

Let me know if you have a better way to do this or if you have any input!

piterden  —  7 years ago

Thank you! If you interest, here is a realization of calendar in Vue, in ES5 style-code. Templates:

<script src="/assets/js/vue.min.js"></script>

<script type="text/x-template" id="calendar-event-modal">
<div v-show="show" id="wantModal" class="modal fade" tabindex="-1" role="dialog">
    <div class="modal-dialog">
        <div class="modal-content">
            <form id="wantForm">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click.stop="$parent.showModal = !$parent.showModal">
                        <span aria-hidden="true">&times;</span>
                    </button>
                    <h4 class="modal-title">Заявка на тренинг</h4>
                    <input type="text" disabled="disabled" class="form-control name-time" id="name-time" name="name-time">
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="name">Имя и фамилия</label>
                        <input type="text" class="form-control" id="name" name="name" data-validation="required">
                    </div>
                    <div class="form-group">
                        <label for="email">Электронная почта</label>
                        <input type="email" class="form-control" id="email" name="email" data-validation="email">
                    </div>
                    <div class="form-group">
                        <label for="phone">Телефон</label>
                        <input type="text" class="form-control" id="phone" name="phone" data-validation="custom" data-validation-regexp="^\+?\d([0-9]{6,10})$" data-validation-error-msg="Ввведите корректный телефон: +71234567890">
                    </div>
                    <button type="submit" class="btn btn-default">Отправить</button>
                </div>
            </form>
        </div>
    </div>
</div>
</script>

<script type="text/x-template" id="calendar-event-vue">
<div class="day" @click="setActiveDate(idx)">
    <div :class="{
        dayNum: true,
        'text-danger': weekDay === 6 || weekDay === 0
    }">{{ date }}</div>
    <div v-if="ev" class="eventItem">
        <span class="eventInfo">
            <span class="eventTitle">{{ ev.title }}</span>
            <span class="eventTime">
                <span class="fromTime">{{ fromTime }}</span> - <span class="toTime">{{ toTime }}</span>
            </span>
        </span>
        <br>
        <em v-if="ev.catname">{{ ev.catname }}</em>
        <transition
            name="slide-down"
            enter-active-class="animated fadeIn"
            leave-active-class="animated fadeOut"
        >
            <div v-show="$parent.current == idx" class="eventCat">
                <div v-if="ev.description" class="eventDesc">
                    <span class="descText" v-html="ev.description"></span>
                    <button type="button" class="btn btn-primary btn-lg" @click.stop="$parent.showModal = !$parent.showModal">
                        Хочу посетить
                    </button>
                </div>
            </div>
        </transition>
    </div>
</div>
</script>

<script type="text/x-template" id="mobile-calendar-event-vue">
<div class="day" @click="setActiveDate(idx)">
    <div :class="{
        dayNum: true,
        'text-danger': weekDay === 6 || weekDay === 0,
        'has-event': ev,
        'active': $parent.current == idx
    }">{{ date }}</div>
    <div v-if="ev" class="eventItem">
        <transition
            name="slide-down"
            enter-active-class="animated fadeIn"
            leave-active-class="animated fadeOut"
            @enter="showStarted"
            @leave="hideStarted"
        >
            <div v-show="$parent.current == idx" class="eventCat">
                <span class="eventInfo">
                    <span class="eventTitle">{{ ev.title }}</span>
                    <em v-if="ev.catname">{{ ev.catname }}</em></br>
                    <span class="eventDate">{{ longDate }}</span>
                    <span class="eventTime">
                        <span class="fromTime">{{ fromTime }}</span> - <span class="toTime">{{ toTime }}</span>
                    </span>
                </span>
                <br>

                <div v-if="ev.description" class="eventDesc">
                    <span class="descText" v-html="ev.description"></span>
                    <button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#wantModal">
                        Хочу посетить
                    </button>
                </div>
            </div>
        </transition>
    </div>
</div>
</script>

<script type="text/x-template" id="calendar-vue">
<div id="my-cal">
    <div class="cal-header">
        <button @click="decMonth()" class="move-month minus">
            <img src="/assets/img/btn-home-calendar-left.gif" alt="Назад">
            <i class="fa fa-chevron-left"></i>
        </button>
        <div id="title-m">{{ monthName + ' \'' + shortYear }}</div>
        <button @click="incMonth()" class="move-month plus">
            <img src="/assets/img/btn-home-calendar-left.gif" alt="Вперёд">
            <i class="fa fa-chevron-right"></i>
        </button>
        <div class="cal-title">Календарь мероприятий</div>
    </div>
    <div class="calendar">
        <div id="calendar-d" class="hidden-sm hidden-xs">
            <div v-for="col in colCount" :class="[ 'cal-col', 'col-xs-' + colWidth, 'cal-col' + col ]">
                <calendar-event v-for="row in getColRowsCount(col)" :col="col" :row="row"></calendar-event>
            </div>
        </div>
        <div id="calendar-m" class="hidden-md hidden-lg">
            <div v-for="row in weeksInMonthCount" :class="[ 'mob-row', 'mob-row' + row ]">
                <mobile-calendar-event v-for="col in getRowColsCount(row)" :col="row > 1 ? col : col + 7 - getRowColsCount(row)" :row="row"></mobile-calendar-event>
            </div>
        </div>
    </div>
    <calendar-event-modal></calendar-event-modal>
</div>
</script>

JavaScript:

+(function(Vue, $, window, document, undefined) {

    Vue.config.devtools = true;

    var calendarEventModal = {
        template: '#calendar-event-modal',

        computed: {
            show: function() {
                return this.$parent.showModal;
            }
        }

    };

    /**
     * Event reusable part
     */
    var CalendarEventMixin = {

        props: {
            col: Number,
            row: Number
        },

        computed: {
            idx: function() {
                return Number(this.date) - 1;
            },
            dateObj: function() {
                return new Date(this.getDateString());
            },
            weekDay: function() {
                return this.dateObj.getDay();
            },
            fromTime: function() {
                return this.ev.startdate.substr(11, 5);
            },
            toTime: function() {
                return this.ev.enddate.substr(11, 5);
            },
            ev: function() {
                var date = this.getDateString();
                console.log(date);
                return this.$parent.events.find(function(event) {
                    return date == event.startdate.substr(0, 10);
                });
            },
            year: function() {
                return this.$root.year;
            },
            month: function() {
                return this.$root.month;
            }
        },

        methods: {
            /**
             * Get double-digits number
             * 
             * @param {Number|String}  number
             */
            nn: function(n) {
                return n > 9 ? String(n) : String('0' + n);
            },

            setActiveDate: function(idx) {
                this.$parent.current = this.$parent.current == idx ? null : idx;
            },

            getDateString: function() {
                return String(this.year) + '-' +
                    String(this.nn(this.month + 1)) + '-' +
                    String(this.nn(this.date));
            }
        }
    };

    /**
     * Event VueJS Component
     */
    var calendarEvent = {
        template: '#calendar-event-vue',
        mixins: [CalendarEventMixin],

        computed: {
            date: function() {
                return this.row + this.col * this.$root.maxInCol - this.$root.maxInCol;
            }
        }
    };

    /**
     * Mobile Event for Calendar
     */
    var mobileCalendarEvent = {
        template: '#mobile-calendar-event-vue',
        mixins: [CalendarEventMixin],

        computed: {
            date: function() {
                var firstRowLen = this.$parent.getRowColsCount(1);

                return (this.row > 1)
                    ? this.col + (this.row - 2) * 7 + firstRowLen
                    : this.col - (7 - firstRowLen);
            },

            longDate: function() {
                return this.date + '.' + Number(this.month + 1) + '.' + this.year + ' г.';
            }
        },

        methods: {

            getFormRow: function() {
                return document.querySelector('.form-wrap');
            },

            hideStarted: function(el) {
                var parent = this.$parent;
                var formRow = this.getFormRow();

                setTimeout(function() {
                    if (!parent.current) {
                        formRow.style.transform = 'translateY(0)';
                    }
                }, 1);
            },

            showStarted: function(el) {
                var formRow = this.getFormRow();

                setTimeout(function() {
                    formRow.style.transform = 'translateY(' + el.offsetHeight + 'px)';
                }, 1);
            },

            setActiveDate: function(idx) {
                var parent = this.$parent;

                parent.current = parent.current == idx ? null : idx;
            }
        }
    };

    /**
     * Calendar VueJS Component
     */
    var calendar = {
        template: '#calendar-vue',
        components: {
            calendarEvent: calendarEvent,
            calendarEventModal: calendarEventModal,
            mobileCalendarEvent: mobileCalendarEvent
        },

        data: function() {
            return {
                monthNames: [ "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
                    "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" ],
                events: [],
                current: null,
                offsetHeight: null,
                showModal: false
            };
        },

        props: {
            dataString: {
                'default': function() {
                    return '[]';
                }
            }
        },

        computed: {

            /**
             * Get col count for desktop
             */
            colCount: function() {
                return this.$root.colCount;
            },

            /**
             * Get bootstrap col width
             */
            colWidth: function() {
                return (this.$root.colCount !== 5) ? 24 / this.$root.colCount : 6;
            },

            /**
             * Get current month name
             */
            monthName: function() {
                return this.monthNames[this.$root.month];
            },

            /**
             * Get current year - 2 digits
             */
            shortYear: function() {
                return this.$root.year.toString().substr(2);
            },

            weeksInMonthCount: function() {
                var firstDayOfFirstWeek = this.$root.firstDay.getDay(),
                    weeksCount = 4;

                // Monday
                if (firstDayOfFirstWeek === 1) {
                    if (this.$root.monthLength > 28) {
                        weeksCount = 5;
                    } else {
                        weeksCount = 4;
                    }
                }

                // Tuesday to Friday
                if (firstDayOfFirstWeek > 1 && firstDayOfFirstWeek < 6 ) {
                    weeksCount = 5;
                }

                // Saturday
                if (firstDayOfFirstWeek === 6) {
                    if (this.$root.monthLength < 31) {
                        weeksCount = 5;
                    } else {
                        weeksCount = 6;
                    }
                }

                // Sunday
                if (firstDayOfFirstWeek === 0) {
                    if (this.$root.monthLength < 30) {
                        weeksCount = 5;
                    } else {
                        weeksCount = 6;
                    }
                }

                return weeksCount;
            }
        },

        mounted: function() {
            this.events = JSON.parse(this.dataString);
            this.offsetHeight = this.getElHeight();
        },

        methods: {

            decMonth: function() {
                this.current = null;
                this.$root.monthMinus();
            },

            incMonth: function() {
                this.current = null;
                this.$root.monthPlus();
            },

            getElHeight: function() {
                return this.$el.offsetHeight;
            },

            /**
             * CSS fix for Mozilla
             * 
             * @param {String}  sel css-selector
             */
            fixOutline: function() {
                var $el = $('.eventDesc span'),
                    oldVal = $el.css('outline');

                $el.css('outline', 'none');

                setTimeout(function() {
                    $el.css('outline', oldVal);
                }, 10);
            },

            /**
             * Get count of dates in one calendar column
             */
            getColRowsCount: function(col) {
                return (col < this.$root.colCount)
                    ? this.$root.maxInCol
                    : this.$root.getMonthLength(this.$root.month, this.$root.year) - this.$root.maxInCol * 2;
            },

            getRowColsCount: function(row) {

                if (row > 1 && row < this.weeksInMonthCount) {
                    return 7;
                }

                var firstDay = this.$root.firstDay.getDay(),
                    lastDay = this.$root.lastDay.getDay();

                if (row === 1) {
                    firstDay = firstDay || 7;
                    return 8 - firstDay;
                }

                if (row === this.weeksInMonthCount) {
                    return lastDay || 7;
                }
            }

        }
    };

    /**
     * Root VueJS Component
     */
    var app = new Vue({
        el: '#vue-app',
        components: { calendar: calendar },

        data: function() {
            return {
                colCount: 3,
                maxInCol: 10,
                month: new Date().getMonth(),
                year: new Date().getFullYear(),
            };
        },

        computed: {
            monthLength: function() {
                return new Date(this.year, this.month + 1, 0).getDate();
            },

            firstDay: function() {
                return new Date(this.year, this.month, 1);
            },

            lastDay: function() {
                return new Date(this.year, this.month + 1, 0);
            }
        },

        methods: {

            /**
             * Decrement month in calendar event callback
             */
            monthMinus: function() {
                if (this.month === 0) {
                    this.month = 11;
                    this.year--;
                } else {
                    this.month--;
                }
            },

            /**
             * Increment month in calendar event callback
             */
            monthPlus: function() {
                if (this.month === 11) {
                    this.month = 0;
                    this.year++;
                } else {
                    this.month++;
                }
            },

            /**
             * Get length of month in days
             * 
             * @param {Number}  month
             * @param {Number}  year
             */
            getMonthLength: function(month, year) {
                return new Date(year, month + 1, 0).getDate();
            },
        },

        mounted: function() {
            this.doOnDocumentReady();
        }
    });

})(Vue, jQuery, window, document);
bloveless  —  7 years ago

Updated the queries to remove some of the whereRaw queries.

{% set years = entries('posts').selectRaw('DISTINCT YEAR(`publish_at`) as year').orderByRaw('YEAR(`publish_at`) desc').get() %}
{% for year in years %}
    {% set yearCount = entries('posts').whereYear('publish_at', year.year).count() %}
    <li class="archive-year {% if year.year == archive_year %}opened{% endif %}">
        <a class="open-year" href="javascript:;"><i class="fa fa-caret-right"></i></a>
        <a href="/posts/archive/{{ year.year }}">{{ year.year }} ({{ yearCount }})</a>

        <ul>
            {% set curYearMonths = entries('posts').selectRaw('DISTINCT MONTHNAME(`publish_at`) as month_name, MONTH(`publish_at`) as month').orderByRaw('MONTH(`publish_at`)').whereYear('publish_at', year.year).get() %}
            {% for month in curYearMonths %}
                {% set monthCount = entries('posts').whereYear('publish_at', year.year).whereMonth('publish_at', month.month).count() %}
                <li class="archive-month {% if month.month == archive_month %}selected{% endif %}">
                    <a href="/posts/archive/{{ year.year }}/{{ month.month }}">
                        <i class="fa fa-caret-right"></i> {{ month.month_name }} ({{ monthCount }})
                    </a>
                </li>
            {% endfor %}
        </ul>
    </li>
{% endfor %}
ryanthompson  —  7 years ago

@brennon might be nice to wrap these into the model criteria for easy access.