How-to: Custom Post Type for Events Pt. 2

Now that we’ve handled the entire registration and back-end functionality (see Pt. 1) of our events custom post type, it’s time to move on to our design output (download the file for this tutorial here)! There’s no point in having fancy custom post types if you can’t display them properly. As you may remember, the entire reason for me requiring this functionality is for my Pub Theme (Pubforce), so I need to give my clients some flexibility with regards to its use. As you can see from the screenshot below, I have the following designs: 1) Featured (Shown with Big Thumbnail), 2) Full Listing (which groups by day) and 3) Widget Listing (again, grouping by day).

custom post types events pub

I don’t want to hinder my end-users in any way so I’ve chosen to use shortcodes to let them decide where and how they want to deploy events within the main area, so let’s get right to that! My tutorial below will revolve around design #2 above.

1) shortcode structure

Shortcodes remind me a lot of Excel macro’s; very detailed scripts serving a specific purpose, yet simple enough that they allow non-developers the ability to pass parameters and make use of them where and how they want. This is a massive plus over something like a page template which is a lot more rigid in its ways.

Incase you’re not all too familiar with shortcodes, I’ve laid out the basic structure below (but be sure to read the relevant codex article):

function shortcode_function ( $atts ) {
	// - define arguments -
	extract(shortcode_atts(array(
		'name' => '',
	 ), $atts));
	// - spit output -
	$output = 'My name is' . $name;
	return $output;
	}
add_shortcode('introduction', 'shortcode_function');
?>

Thus, if I were to now insert [introduction name=”Noel”] in a post, I’d get “My name is Noel“. Really hard right? As the code is going to get really long after, I’m just going to take it one step further to show you the shell of the shortcode we’ll create today. We’re going to use a nifty little function called Output Buffer (which just collects everything that is produced, starting with ob_start() and ending with ob_get_contents()). This will save us from having to piece everything together a different way (shortcodes require the use of ‘return‘, hence all this messing around):

function tf_events_full ( $atts ) {
	// - define arguments -
	extract(shortcode_atts(array(
		'limit' => '10', // Default amount of events to show
	 ), $atts))
	// - start grabbing output -
	ob_start();
	// - generate output -
	... next step of the tutorial ...
	// - spit output -
	$output = ob_get_contents();
	ob_end_clean();
	return $output;
	}
add_shortcode('tf-events-full', ' tf_events_full');
?>

Now that we have the shell of our shortcode, lets move on to more interesting things shall we.

2) query

Although tradition would have dictated I use a built-in function such as wp_query, I wanted to leave my options down the road without having to rewrite everything. This is why I’m using a custom select query which lets you do anything you want (more or less).

// - hide events that are older than 6am today -
$today6am = strtotime('today 6:00') + ( get_option( 'gmt_offset' ) * 3600 );
// - query -
global $wpdb;
$querystr = "
    SELECT *
    FROM $wpdb->posts wposts, $wpdb->postmeta metastart, $wpdb->postmeta metaend
    WHERE (wposts.ID = metastart.post_id AND wposts.ID = metaend.post_id)
    AND (metaend.meta_key = 'tf_events_enddate' AND metaend.meta_value > $today6am )
    AND metastart.meta_key = 'tf_events_enddate'
    AND wposts.post_type = 'tf_events'
    AND wposts.post_status = 'publish'
    ORDER BY metastart.meta_value ASC LIMIT $limit
 ";
$events = $wpdb->get_results($querystr, OBJECT);

You’ll notice right at the top that I’m defining this variable called $today6am. Given my clients all run Pubs, I figured this would be a good time to make events disappear. I like the concept of removing dates on a daily basis as opposed to when they occur, and seeing as many events go into the early morning, I found 6am to be a happy medium to remove events that happened on the day before. You’ll also see that I’m conscious of the local timezone by multiplying the timezone difference in hours (gmt_offset) by how many seconds there are in an hour (3600). Adding this value to the Unix timestamp means it’s always 6am local time. Also note that I’m comparing against the event end time (as some events are full-day).

It’s also important that we call the postmeta twice (metastart & metaend), this way we can use the start & end time for their respective purposes; start for sorting the events and end for checking if they have expired.

Also, you’ll remember that $limit is a parameter passed through the shortcode (default ’10’).

3) loop

Now the loop itself, as mentioned above this is the final look we’re trying to achieve:

custom post types wordpress

In order to do that, this is the code we’ll require (starting off with the loop declaration itself and any variables we’d like to use in our final output):

// - declare fresh day -
$daycheck = null;
// - loop start -
if ($events):
global $post;
foreach ($events as $post):
setup_postdata($post);
// - custom variables -
$custom = get_post_custom(get_the_ID());
$sd = $custom["tf_events_startdate"][0];
$ed = $custom["tf_events_enddate"][0];
// - determine if it's a new day -
$longdate = date("l, F j, Y", $sd);
if ($daycheck == null) { echo '<h2 class="full-events">' . $longdate . '</h2>'; }
if ($daycheck != $longdate && $daycheck != null) { echo '<h2 class="full-events">' . $longdate . '</h2>'; }
// - local time format -
$time_format = get_option('time_format');
$stime = date($time_format, $sd);
$etime = date($time_format, $ed);
?>

… followed by the XHTML output …

<div class="full-events">
    <div class="text">
        <div class="title">
            <div class="time"><?php echo $stime . ' - ' . $etime; ?></div>
            <div class="eventtext"><?php the_title(); ?></div>
        </div>
    </div>
     <div class="desc"><?php if (strlen($post->post_content) > 150) { echo substr($post->post_content, 0, 150) . '...'; } else { echo $post->post_content; } ?></div>
</div>

… and finally the end of the loop …

<?php
// - fill daycheck with the current day -
$daycheck = $longdate;
// - go back to the top -
endforeach;
else :
endif;

You’re probably wondering what all those ‘day’ variables are? Well they’re there to help us group the events into days like the design above! Let me break it down so it makes more sense (see commenting):

// - declare fresh day -
$daycheck = null;
... loop starts ...
// - convert date -
$longdate = date("l, F j, Y", $sd);
// - if it's our first event, echo date as title -
if ($daycheck == null) { echo '<h2 class="full-events">' . $longdate . '</h2>'; }
// - if the date has changed since the last post, echo date as title -
if ($daycheck != $longdate && $daycheck != null) { echo '<h2 class="full-events">' . $longdate . '</h2>'; }
... output rest of event ...
// - fill daycheck with the current day -
$daycheck = $longdate;
... loop ends and repeats ...

4) bonus: featured loop

Now that we’ve got the regular event listing done, I figured I’d go an extra step to show you how a featured event is displayed. The big difference here is that we’ll be using the custom taxonomy that we created to help filter results. Specifically, the end user will be able to select ‘Featured’ to trigger the required visibility:

Instead of repeating the entire code, I’m just going to show you the actual query, which is the most important change (as it now involves pulling in taxonomy data):

global $wpdb;
$querystr = "
SELECT *
FROM $wpdb->postmeta metastart, $wpdb->postmeta metaend, $wpdb->posts
LEFT JOIN $wpdb->term_relationships ON($wpdb->posts.ID = $wpdb->term_relationships.object_id)
LEFT JOIN $wpdb->term_taxonomy ON($wpdb->term_relationships.term_taxonomy_id = $wpdb->term_taxonomy.term_taxonomy_id)
LEFT JOIN $wpdb->terms ON($wpdb->terms.term_id = $wpdb->term_taxonomy.term_id)
WHERE ($wpdb->posts.ID = metastart.post_id AND $wpdb->posts.ID = metaend.post_id)
AND $wpdb->term_taxonomy.taxonomy = 'tf_eventcategory'
AND $wpdb->terms.name = '$group'
AND (metaend.meta_key = 'tf_events_enddate' AND metaend.meta_value > $today6am )
AND metastart.meta_key = 'tf_events_enddate'
AND $wpdb->posts.post_type = 'tf_events'
AND $wpdb->posts.post_status = 'publish'
ORDER BY metastart.meta_value ASC LIMIT $limit
";
$events = $wpdb->get_results($querystr, OBJECT);

You’ll notice I’ve also got a new variable in the mix called $group, this is by default ‘Featured’, however can be overridden by the user through the shortcode (maybe they only want to show Soccer matches). This is the sort of flexibility you want to give to your clients.

It would have also been possible to just create a checkbox within the custom post type, but again we’d be limiting user options and not making use of well-functioning WP code.

5) your turn to speak!

Your options are endless with this setup (you can downloaded the entire file here), but you’ll always need to beware of the amount of queries it runs as it can really be taxing on the server (check out my post on my development environment for more information on debugging).

If you have any suggestions, comments or ways you’d do things differently, please let me know by commenting below! I’d love to hear your take on this as well as your experiences. Feel free to ping me on Twitter @noeltock or retweet this message, always appreciated!