Create a Weekly view custom Calendar in WordPress

For a recent project, I needed to develop a calendar with a weekly view that would display events based on 3 different custom post types.
Since the website required a very custom setup, I didn’t want to use an event plugin, since it would have limited the amount of customization that the client needed.

So once the design was ready, the tricky part kicked in: developing a light & dynamic calendar, in a weekly view, with current day & events highlighted, that would show different types of events happening each day.

wordpress weekly calendar php javascript

Thankfully PHP (specifically PHP 5.3+ for Relative Formats) gave me all the necessary tools:

time()
mktime()
strtotime()
Relative Formats
NumberFormatter Class

The resulting HTML & PHP, already integrated with some custom WordPress queries (that you will need to adjust to your own needs), is in this Gist.

<div id="calendar">
<?php
// Dates
$year = date('Y');
$first_month_this_year_ts = mktime(0, 0, 0, date("F", strtotime('first month of this year')) + 1, date('d'), date('Y')); // timestamp
$today = date("j F Y", strtotime('today')); // human date
$today_ts = strtotime('today'); // timestamp
$today_day_of_year = date("z", $today_ts);// int
// Calculate number of weeks in a year
function num_weeks_in_year($year) {
$daySum = 0;
for ($x = 1; $x <= 12; $x++)
$daySum += cal_days_in_month(CAL_GREGORIAN, $x, $year);
return $daySum / 7; // int
}
$num_weeks_in_year_int = substr(num_weeks_in_year($year), 0, 2); // 52
$current_week_n = idate('W', mktime(0, 0, 0, date('m'), date('d'), date('Y'))); // 28
// Returns the "adjusted" number of weeks in a month, for the weekly view (some months will have 5 weeks since months dont always start on a Monday)
function total_weeks_in_month($year, $month, $start_day_of_week) {
// Total number of days in the given month.
$num_of_days = date("t", mktime(0,0,0,$month,1,$year));
// Count the number of times it hits $start_day_of_week.
$num_of_weeks = 0;
for($i=1; $i<=$num_of_days; $i++) {
$day_of_week = date('w', mktime(0,0,0,$month,$i,$year));
if($day_of_week==$start_day_of_week)
$num_of_weeks++;
}
return $num_of_weeks;
}
?>
<div class="calendar">
<div class="calendarMonthWrap">
<!-- Months -->
<div class="calendarMonth">
<?php
// Get all months of the year
$first_month_this_year_monthloop_ts = $first_month_this_year_ts;
for ($m = 1; $m <= 12; $m++) {
// How many "adjusted" weeks this months? end and beginning of 2 months could be in the same week view
$total_weeks = total_weeks_in_month($year, $m,0);
$month = strftime('%B', $first_month_this_year_monthloop_ts);
$first_month_this_year_monthloop_ts = strtotime('+1 month', $first_month_this_year_monthloop_ts);
// Current Month Class
if ($m == 0) {
$month_class = 'first';
} elseif ($m == 11) {
$month_class = 'last';
} elseif ($month == date("F", strtotime('today'))) {
$month_class = 'active-month';
} else {
$month_class = '';
}
echo '<div class="month ' . $month_class . '">' . date_i18n("F", strtotime($month)) . ' <strong>' . date('Y') . '</strong>';
?>
<!-- Weeks -->
<div class="weekWrap">
<div class="calendarWeek">
<?php
// Get all weeks of the year
$first_day_of_month = new DateTime('first day of ' . date("F", strtotime($month)) . ' ' . date('Y'));
// Since the first day may not be a Monday, let's find the monday right before the first month day
$first_day_of_month_ts = strtotime($first_day_of_month->format('j F Y'));
for ($w = 0; $w < $total_weeks; $w++) {
// We need this value to update as we loop
if($w != 0 ){
$first_day_of_month_ts = strtotime('+1 week', $first_day_of_month_ts);
} else {
$first_day_of_month_ts = strtotime('+0 week', $first_day_of_month_ts);
}
// Week div classes
$week_of_day_class = '';
$week_active = '';
if ($w == 0) {
$week_of_day_class = ' first_week_of_month ';
$week_active = '';
} elseif (($w + 1) == $total_weeks) {
$week_of_day_class = ' last_week_of_month ';
$week_active = '';
}
if (($w + 1) == ceil( date( 'j', strtotime( 'today' ) ) / 7 ) && date("F", strtotime('today')) == $month) {
$week_active = ' weeksActive';
} elseif ($w == 0 && date("F", strtotime('today')) != $month) {
}
// Start Week block
echo '<div class="weeks w' . ($w + 1) . $week_of_day_class . $week_active . '">';
echo '<table border="0" cellpadding="5" cellspacing="0">';
// Get week days first letter
echo '<tr id="week" class="day">';
for ($d = 0; $d < 7; $d++) {
$day_long_ts = strtotime('monday +' . $d . ' day this week ' . date('Y-m-d', $first_day_of_month_ts));
$day_long = date_i18n("D", $day_long_ts);
$day = $day_long[0];
// Current Day Class
$day_of_the_year = date("z", $day_long_ts);
if ($today_day_of_year == $day_of_the_year) {
$current_class = ' current';
} else {
$current_class = '';
}
echo '<td class="d' . $day_of_the_year . $current_class . '">' . $day . '</td>';
}
echo '</tr>';
// Get week day number
echo '<tr id="day" class="day">';
for ($d = 0; $d < 7; $d++) {
$dayn_ts = strtotime('monday +' . $d . ' day this week ' . date('Y-m-d', $first_day_of_month_ts));
$dayn = date_i18n("j", $dayn_ts);
// Current Day N Class
$day_of_the_year = date("z", $dayn_ts);
if ($today_day_of_year == $day_of_the_year) {
$current_class = ' current';
} else {
$current_class = '';
}
echo '<td class="d' . $day_of_the_year . $current_class . '">' . $dayn . '</td>';
}
echo '</tr>';
// Get calendar Events
echo '<tr class="C-event">';
for ($d = 0; $d < 7; $d++) {
$weekday_ts = strtotime('monday +' . $d . ' day this week ' . date('Y-m-d', $first_day_of_month_ts));
$day_of_the_year = date("z", $weekday_ts);
$beginning_of_day = strtotime("midnight", $weekday_ts);
$end_of_day = strtotime("tomorrow", $beginning_of_day) - 1;
// Check if values are cached, if not cache them
$events_today = 'custom_weekly_cal_' . date('j', $weekday_ts) . date('F', $weekday_ts) . date('Y', $weekday_ts) . '_6h';
//delete_transient($events_today);
if (get_transient($events_today) === false) {
// Get posts for each day
$loop_news = new WP_Query(array(
'posts_per_page' => 1,
'post_type' => 'post',
'meta_query' => array(
array(
'key' => 'wpcf-data_inizio', // custom field with event start timestamp
'value' => array($beginning_of_day, $end_of_day),
'compare' => 'BETWEEN',
'type' => 'numeric'
),
)
));
// Get events for each day
$loop_events = new WP_Query(array(
'posts_per_page' => 1,
'post_type' => 'evento',
'meta_query' => array(
array(
'key' => 'wpcf-data_inizio',
'value' => array($beginning_of_day, $end_of_day),
'compare' => 'BETWEEN',
'type' => 'numeric'
),
)
));
// Get corsi for each day
$loop_corsi = new WP_Query(array(
'posts_per_page' => 1,
'post_type' => 'corso',
'meta_query' => array(
array(
'key' => 'wpcf-data_inizio',
'value' => array($beginning_of_day, $end_of_day),
'compare' => 'BETWEEN',
'type' => 'numeric'
),
)
));
// Final Query
$final_query = new WP_Query();
// Merging queries
$final_query->posts = array_merge($loop_news->posts, $loop_events->posts, $loop_corsi->posts);
// Recount
$final_query->post_count = count($final_query->posts);
// Cache Results
set_transient($events_today, $final_query, 6 * HOUR_IN_SECONDS);
}
$final_query = get_transient($events_today);
$post_count = $final_query->post_count;
// Convert number into letters
if ($post_count > 0) {
$f = new NumberFormatter("en", NumberFormatter::SPELLOUT);
$num_events = $f->format($post_count) . 'Event';
} else {
$num_events = '';
}
// Current Event Class
if ($today_ts == $weekday_ts) {
$current_class = ' current';
} else {
$current_class = '';
}
// Event dots wrapper
echo '<td class="' . $num_events . ' d' . $day_of_the_year . ' ' . $current_class . '">';
if ($post_count > 0) {
if ($final_query->have_posts()) :
while ($final_query->have_posts()) : $final_query->the_post();
$post_type = get_post_type();
// Colore
if ($post_type == 'post') {
$post_color_class = CLASS_CALENDAR_POST;
$post_data_class = DATA_CALENDAR_POST;
} elseif ($post_type == 'evento') {
$post_color_class = CLASS_CALENDAR_EVENTO;
$post_data_class = DATA_CALENDAR_EVENTO;
} elseif ($post_type == 'corso') {
$post_color_class = CLASS_CALENDAR_CORSO;
$post_data_class = DATA_CALENDAR_CORSO;
} elseif ($post_type == 'prenotazione') {
$post_color_class = CLASS_CALENDAR_PRENOTAZIONE;
$post_data_class = DATA_CALENDAR_PRENOTAZIONE;
} else {
$post_color_class = '';
$post_data_class = '';
}
echo '<span data-Name="' . get_the_title() . '" data-eventcolor="' . $post_data_class . '" class="eventCricle ' . $post_color_class . '"></span>';
endwhile;
endif; wp_reset_query(); wp_reset_postdata();
}
echo '</td>';
} // end of daily Events
echo '</tr>';
?>
<?php
// End of Week Block
echo '</table></div>';
}
?>
</div>
</div>
<?php
echo '</div>';
}
?>
</div>
<!-- Calendar Navigation -->
<div class="prev month-nav"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div class="next month-nav"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
<div class="prev week-nav"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div class="next week-nav"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
</div>
</div>
<!-- Title of events / placeholder -->
<div class="bottomtext">
<?php
// Placeholder Events for today
$placeholder_query = get_transient('custom_weekly_cal_' . date('j') . date('F') . date('Y') . '_6h');
if ($placeholder_query) {
if ($placeholder_query->have_posts()) : while ($placeholder_query->have_posts()) : $placeholder_query->the_post();
the_title();
endwhile;
endif; wp_reset_query(); wp_reset_postdata();
}
?>
</div>
</div>

You will notice the use of WordPress transients, this is to cache the queries as much as possible to improve performance and page load.

The calendar can scroll by month and by week obviously, on click of a certain day it also highlights it and adds a dot inside the circles, which represent all the different types of event, showing at the bottom their title.

Now, the original version was a gold design, so here is the CSS for it.

div#calendar {
text-align: center;
clear: both;
position: relative;
border-bottom: 3px solid #7A6E4D;
}
#calendar:after {
content: "";
width: 2px;
height: 50px;
display: block;
background-color: #7A6E4D;
margin: auto;
}
#calendar:before {
content: "";
width: 100px;
height: 2px;
position: absolute;
bottom: 50px;
display: block;
background-color: #7A6E4D;
margin: auto;
left: 0;
right: 0;
}
.bottomtext {
color: #486793;
text-transform: uppercase;
letter-spacing: 2px;
margin: 10px 0;
font-weight: 600;
}
.bottomtext > span {
display: block;
}
.calendar {
max-width: 500px;
margin: auto;
font-size: 32px;
color: #7A6E4D;
text-transform: uppercase;
letter-spacing: 4px;
}
.calendarMonth {
width: 100%;
position: relative;
height: auto;
border-bottom: none;
}
.calendarWeek {
width: 100%;
position: relative;
height: auto;
}
.calendarMonth .month, .calendarWeek .weeks {
opacity: 0;
transition: all .2s;
position: absolute;
top: 0;
transform: translateX(-100%);
-webkit-transform: translateX(-100%);
-moz-transform: translateX(-100%);
}
.calendarMonth .active-month + .month, .calendarWeek .weeksActive + .weeks {
transform: translateX(100%);
-webkit-transform: translateX(100%);
-moz-transform: translateX(100%);
}
.calendarWeek, .calendarMonth {
overflow: hidden;
position: relative;
}
.calendarMonth .month {
padding: 20px 0 0;
}
.active-month, .weeksActive {
display: block !important;
width: 100% !important;
opacity: 1 !important;
height: auto !important;
position: relative !important;
transform: none !important;
}
.month-nav, .week-nav {
position: absolute;
top: 12px;
margin: 0;
height: auto;
font-size: 40px;
cursor: pointer;
}
.prev.week-nav {
position: absolute;
left: 0;
top: 138px;
}
.next.week-nav{
position: absolute;
right: 0;
top: 138px;
}
.prev.month-nav, .prev.week-nav {
left: -60px;
}
.calendarMonthWrap, .weekWrap {
position: relative;
}
.weekWrap {
border-top: 2px solid #7A6E4D;
}
.next.month-nav, .next.week-nav {
right: -60px;
}
.calendar table {
width: 100%;
table-layout: fixed;
}
.weeks td {
border-bottom: 2px solid;
height: 70px;
}
.day td{
cursor: pointer;
}
.day td.current {
font-weight: 800;
}
span.eventCricle {
border: 3px solid;
width: 20px;
height: 20px;
display: inline-block;
border-radius: 50%;
position: relative;
}
span.eventCricle.circleRed {
border-color: #C64F51;
}
span.eventCricle.circleGoBen {
border-color: #7D714B;
}
span.eventCricle.circleBlue {
border-color: #486793;
}
span.eventCricle.circlegreen {
border-color: #74A25A;
}
td.twoEvent > span {
display: block;
clear: both;
}
td.twoEvent > span:first-child {
float: right;
clear: both;
}
td.threeEvent > span {
clear: both;
display: block;
}
td.threeEvent > span:first-child {
float: right;
}
td.threeEvent > span:nth-child(2) {
margin: auto;
}
td.current .eventCricle:after {
content: "";
width: 0px;
height: 0px;
border: 3px solid;
display: block;
border-color: inherit !important;
border-radius: 50%;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
.week-nav {
margin: initial;
top: 40px;
}
CSS

And finally, the Javascript which handles the scrolling and active year/month/day/events transitions.

$('.prev.month-nav').click(function () {
$(".month.active-month .weeks").prevAll().removeClass("weeksActive");
$(".month.active-month .weeks").nextAll().removeClass('weeksActive');
$(".month.active-month").prev().addClass("active-month");
$(".month.active-month").nextAll().removeClass('active-month');
$(".month.active-month").prevAll().removeClass("active-month");
$(".month.active-month").find(".last_week_of_month").addClass("weeksActive");
});
$('.next.month-nav').click(function () {
$(".month.active-month .weeks").prevAll().removeClass("weeksActive");
$(".month.active-month .weeks").nextAll().removeClass('weeksActive');
$(".month.active-month").next().addClass("active-month");
$(".month.active-month").prevAll().removeClass("active-month");
$(".month.active-month").nextAll().removeClass('active-month');
$(".month.active-month").find(".first_week_of_month").addClass("weeksActive");
});
$('.prev.week-nav').click(function () {
if ($(".month.active-month .weeks.weeksActive").prev().is(".weeks")) {
$(".month.active-month .weeks.weeksActive").prev().addClass("weeksActive");
$(".month.active-month .weeks.weeksActive").nextAll().removeClass('weeksActive');
$(".month.active-month .weeks.weeksActive").prevAll().removeClass("weeksActive");
} else {
$('.prev.month-nav').trigger("click");
}
});
$('.next.week-nav').click(function () {
if ($(".month.active-month .weeks.weeksActive").next().is('.weeks')) {
$(".month.active-month .weeks.weeksActive").next().addClass("weeksActive");
$(".month.active-month .weeks.weeksActive").prevAll().removeClass("weeksActive");
$(".month.active-month .weeks.weeksActive").nextAll().removeClass('weeksActive');
} else {
$('.next.month-nav').trigger("click");
}
});
$('.day td').click(function () {
var eventHTML = '';
var id = $(this).attr("class");
$('.active-month .weeksActive .day .' + id + '').addClass('current');
$('.active-month .weeksActive .C-event .' + id + '').addClass('current');
$('.active-month .weeksActive .day .' + id + '').prevAll().removeClass("current");
$('.active-month .weeksActive .C-event .' + id + '').prevAll().removeClass("current");
$('.active-month .weeksActive .day .' + id + '').nextAll().removeClass('current');
$('.active-month .weeksActive .C-event .' + id + '').nextAll().removeClass('current');
if ($('.active-month .weeksActive .C-event .current').find('span').length > 0) {
$('.active-month .weeksActive .C-event .current span').each(function (value) {
var color = $(this).attr("data-eventcolor");
var eventName = $(this).attr("data-name");
eventHTML += '<span class="' + color + '">' + eventName + '</span>';
});
} else {
eventHTML += '';
}
$('.bottomtext').html(eventHTML);
});
$('.C-event td span').click(function () {
var cEventClasses = $(this).parent().attr('class').split(/\s/);
var cEventClass = cEventClasses[1];
$('.active-month .weeksActive .week td').removeClass('current');
$('.active-month .weeksActive .day td').removeClass('current');
$('.active-month .weeksActive .week .' + cEventClass).addClass('current');
$('.active-month .weeksActive .day .' + cEventClass).addClass('current');
$('.C-event td').removeClass('current');
$('.active-month .weeksActive .C-event td.' + cEventClass + '').addClass('current');
var eventHTML2 = '';
if ($('.active-month .weeksActive .C-event .current').find('span').length > 0) {
$('.active-month .weeksActive .C-event .current span').each(function (value) {
var color = $(this).attr("data-eventcolor");
var eventName = $(this).attr("data-name");
eventHTML2 += '<span class="' + color + '">' + eventName + '</span>';
});
} else {
eventHTML2 += '';
}
$('.bottomtext').html(eventHTML2);
});
JavaScript

Now that you have your sweet weekly calendar, you might also be interested in creating a handy .ics calendar file, for all your events.

Media Temple Hosting

The Author

Jany Martelli

Solutions Architect, Innovation Consultant, Developer, Professor.
From digital business consulting, to the development of custom IT solutions, to creating the optimal digital corporate environment: I help companies work better and faster, with custom digital tools and comprehensive innovation strategies, since 2009. I work every day with companies worldwide, from SME to corporate. At IED University, I teach how take full advantage of Technology in Digital Communication.