Notes from a Newbie 11: View Blogs

Notes from a Newbie document the creation and deployment of yardbirdfanclub.org with Perl Catalyst on shared hosting. They are intended for a Perl Catalyst Newbie who would like to study the creation and deployment of a simple Perl Catalyst application.

We experimented then started over and now have a decent looking application with authentication/authorization features:

  • Create new member
  • Login member
  • Logout member

We also have other things going, such as a complete database with all tables and columns needed to record membership and create and use blog entries and comments. We know the difference between DBIC Row, Result and ResultSets, and how to use them to store and retrieve data. We've used both an HTML::FormHandler form and one we rolled ourselves. We have header and footer templates to minimize code duplication while providing an attractive, consistent look to our pages. We have a good start on a css stylesheet, with a few basic styles we've been using in our templates.

In our experiments we figured out how to display and link to blog titles and content, with little concern for their appearance on a page. We now readdress the issue of displaying them, with a few new styles, search and sort routines for improvements.

AutoCRUD

We won't add the feature to create blogs until after adding features to view them, therefore we will use AutoCRUD in the meantime. We will want a few entries to experiment with as we add features to view them.

All it takes to use AutoCRUD is two things:

1) Use AutoCRUD in your application class:

Yardbird/lib/Yardbird.pm:

use Catalyst qw/
  -Debug
  ConfigLoader
  Static::Simple

  AutoCRUD

  Authentication
  Authorization::Roles

  Session
  Session::Store::File
  Session::State::Cookie 
/;

2) Give your browser the location to use AutoCRUD:

http://0.0.0.0:3000/autocrud

Run your application with autocrud and create a few blog entries to test with. They will be useful as we add features to view them.

View Recent Blog Entries

We are ready to add Blog features, where shall we start?

When a user, whether logged-in or not, selects 'Blog' from the main menu, let's have the most recent blog entries displayed, ordered with the most recent at the top.

We did this already in Experiment 02: Create ResultSet Class (Query) Search Methods, but this time we will put effort into formatting page output to make it look decent. We will also be concerned with URI paths as we add our application features.

We already created all the ResultSet search methods we need in Experiment 02: Create ResultSet Class (Query) Search Methods. Copy the Blog ResultSet file over to our new application from Experiment 02 or use this one, they are the same file:

Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm:

package Yardbird::Schema::ResultSet::Blog;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';

sub specific {
  my ($self, $bID) = @_;

  return $self->search({id => $bID});
} 

sub most_recent {
  my ($self) = @_;

  return $self->search({}, {order_by => {-desc => ['id']}, rows => 4});
} 

sub all_user {
  my ($self, $uID) = @_;

  return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
} 

1;

We will create a new controller, and index method to search for and retrieve a list of Row objects to put in the stash for a template to use. These Row objects will contain our blog entries.

Create a controller:

Yardbird]$ script/yardbird_create.pl controller Blog

Edit the new Blog.pm controller's index method to search for the most recent blog entries and return the results in the stash. Since the method call is in list context, a list of row objects is returned and put in the stash:

Yardbird/lib/Yardbird/Controller/Blog.pm:

sub index :Path :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]); 
  $c->detach($c->view("TT")); 
}

We won't create any Sidebar Menus yet but will soon do so, therefore we setup our page to display content with sidebar:

Yardbird/root/src/blog/index.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title">[% blog_entry.title %]</p>
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Add these styles to the stylesheet:

Yardbird/root/static/css/main.css:

.content_title {
  font-size: 1.4em;
  font-weight: bold;
  margin-top: 0.3em;
}

.content_subtitle {
  font-size: 0.8em;
  margin-top: -1.6em;
}

Enable a link to blog/index:

Change this:

Yardbird/root/src/header.tt:

<li>Blog</li>

to this:

<li><a href="[% c.uri_for_action('blog/index') %]">Blog</a></li>

Run it and you should get a decent page with most recent blog entries at the top:

yfc11b1.png

View a Specific Blog Entry

Now that we have the basic blog page format down, we can begin concentrating on user-interface details such as:

  • How to view any specific blog entry?
  • How to create, edit and delete blog entries?

How would you implement these features? Enable a search by title or author? Select from a long list of blog entries? Ouch, what if we have hundreds or thousands of blog entries?

I decided to try and have an interface similar to blogs.perl.org. Rather than confusing you with a poor explanation, please go to yardbirdfanclub.org and see for yourself how to become a member, and view, create, edit and delete blog entries and comments. Use the site and learn how the interface works, then come back and we'll continue to build this application.

Click on Title in Main Content Area

We want to be able to click on the title of any blog entry in the main content area of our blog page, then go to the blog/entries page for that entry.

Let's begin by creating a new blog/entries controller, and index method to display the entry of a selected title:

Yardbird]$ script/yardbird_create.pl controller Blog::Entries

Yardbird/lib/Yardbird/Controller/Blog/Entries.pm:

sub index :Path :Args(1) {
  my ($self, $c, $bID) = @_;

  my $row = $c->model('DB::Blog')->specific($bID)->first;
  $c->stash(blog_entry => $row);
  $c->detach($c->view("TT")); 
}

Our new blog/entries/index template will display a specific blog entry in the main content area:

Yardbird/root/src/blog/entries/index.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_entry.userid.name %]
<div id="header">
  <div id="header_title">[% blog_entry.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="content_with_sidebar">
    <p class="content_title">[% blog_entry.title %]</p>
    <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
    <p>[% blog_entry.content %]</p>
  </div>
</div>

[% INCLUDE footer.tt %]

Make main content blog entry titles into links:

Change this:

Yardbird/root/src/blog/index.tt:

<p class="content_title">[% blog_entry.title %]</p>

to this:

<p class="content_title"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p>

We want the titles to appear as links so we add a few new styles to our stylesheet:

Yardbird/root/static/css/main.css:

.content_title a {
  color: #004080;
  text-decoration: none; 
}

.content_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

Run it and you should get a decent blog/entries page when clicking a blog page main content blog entry title:

yfc11c1.png

Click on Title in Sidebar Menu

blog/entries Page

Let's add a sidebar menu to the blog/entries page, containing titles of all blog entries authored by the member who's blog entry is being displayed. Selecting a title will cause it's corresponding blog entry to be displayed in the main content area of the same page.

Our controller will need to put a list of row objects in the stash for our template, corresponding to all of a member's blog entries.

Add this line to your index method:

$c->stash(blog_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);

After adding it you should have this:

Yardbird/lib/Yardbird/Controller/Blog/Entries.pm:

sub index :Path :Args(1) {
  my ($self, $c, $bID) = @_;

  my $row = $c->model('DB::Blog')->specific($bID)->first;
  $c->stash(blog_entry => $row);
  $c->stash(blog_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);
  $c->detach($c->view("TT")); 
}

Add a new sidebar menu to the template:

Yardbird/root/src/blog/entries/index.tt:

<div id="sidebar_wrapper">
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      [% FOREACH entry IN blog_entries %]
        <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', entry.id) %]">[% entry.title %]</a></p>
        <p class="sidebar_item_subtitle">[% entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      [% END %]
    </div>
  </div>
</div>

After doing so you should have this:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_entry.userid.name %]
<div id="header">
  <div id="header_title">[% blog_entry.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">
    <div class="sidebar_item">
      <div class="sidebar_item_content">
        [% FOREACH entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', entry.id) %]">[% entry.title %]</a></p>
          <p class="sidebar_item_subtitle">[% entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>
  </div>

  <div id="content_with_sidebar">
    <p class="content_title">[% blog_entry.title %]</p>
    <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
    <p>[% blog_entry.content %]</p>
  </div>
</div>

[% INCLUDE footer.tt %]

Our new sidebar menu needs styles which we add to our stylesheet:

Yardbird/root/static/css/main.css:

.sidebar_item_title {
  font-size: 0.9em;
  font-weight: bold;
}

.sidebar_item_title a {
  color: #004080;
  text-decoration: none; 
}

.sidebar_item_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

.sidebar_item_subtitle {
  font-size: 0.65em;
  margin-top: -1.5em;
}

Run it and you should get an attractive, functional sidebar menu on the blog/entries page:

yfc11d1.png

blog Page

We also need to add a sidebar menu to the blog page, containing titles that link to the most recent blog entries on that page.

Our Blog controller index method is already putting the most recent blog entries into the stack, and requires no other changes:

Yardbird/lib/Yardbird/Controller/Blog.pm:

sub index :Path :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);
  $c->detach($c->view("TT")); 
}

It's corresponding template is where we add the new feature, this is what we have now:

Yardbird/root/src/blog/index.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p> 
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

We will first add ID selectors to identify the blog entries to link to, then create the new sidebar menu with links to the blog entries identified by these ID selectors.

First add ID selectors to identify blog entries to link to. I concatenate 'bid' with blog_entry.id so that the resulting ID selector does not begin with a number. Some browsers do not recognize ID selectors that begin with a number.

id="[% 'bid' _ blog_entry.id %]"

After adding the ID selectors we have:

Yardbird/root/src/blog/index.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p> 
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Then we add the new sidebar menu:

<div id="sidebar_wrapper">
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      <p><strong>Recent Blog Entries</strong></p>
      [% FOREACH blog_entry IN blog_entries %]
        <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
        <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
        <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      [% END %]
    </div>
  </div>
</div>

After adding the new sidebar menu we have:

Yardbird/root/src/blog/index.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">
    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Recent Blog Entries</strong></p>
        [% FOREACH blog_entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
          <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
          <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>
  </div>

  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p> 
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Notice the use of our new sidebar_item_sub_subtitle class above. We need to add the corresponding style:

Yardbird/root/static/css/main.css:

.sidebar_item_sub_subtitle {
  font-size: 0.65em;
  margin-top: -1.0em;
}

Run it and you should get a decent looking sidebar menu on the blog page, with links to the recent blog entries on that page:

yfc11e1.png

If (like me) you don't have enough content in your blog entries to fill up a page, you will need to add some to test it.

After adding "A Long Blog Entry" I clicked on it's title in the Recent Blog Entries sidebar menu and got this:

yfc11e2.png

Click on Member Name in Sidebar Menu

Recall that we use a rather complicated means to determine a user's name:

Yardbird/lib/Yardbird/Schema/Result/User.pm:

sub name {
  my ($self) = @_;
  my $bestname;

  if ($self->firstname) {
    $bestname = $self->firstname;
    if ($self->lastname) {
      $bestname .= ' ' . $self->lastname;
    }
  }
  elsif ($self->lastname) {
    $bestname = $self->lastname;
  }
  else {
    $bestname = $self->username;
  }
  return $bestname;
}

This makes it a little more difficult to sort by member names:

my @members;
for my $user ($c->model('DB::User')->all) {
  my $blog_entry = $c->model('DB::Blog')->all_user($user->id)->first;
  push @members, { name => $user->name, bid => $blog_entry->id } if $blog_entry; 
}
my @sorted_members = sort { "\L$a->{name}" cmp "\L$b->{name}" } @members; 
$c->stash(sorted_members => [@sorted_members]);

We loop through the users. If a user has any blog entries, we add their name and blog id of their most recent blog entry to @members. When done looping through all users we sort @members and put the result in the stash for our template.

Add the code to our controller index method and we have:

Yardbird/lib/Yardbird/Controller/Blog.pm:

sub index :Path :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);

  my @members;
  for my $user ($c->model('DB::User')->all) {
    my $blog_entry = $c->model('DB::Blog')->all_user($user->id)->first;
    push @members, { name => $user->name, bid => $blog_entry->id } if $blog_entry; 
  }
  my @sorted_members = sort { "\L$a->{name}" cmp "\L$b->{name}" } @members; 
  $c->stash(sorted_members => [@sorted_members]);

  $c->detach($c->view("TT")); 
}

Add the new sidebar menu to the template:

Yardbird/root/src/blog/index.tt:

<div class="sidebar_item">
  <div class="sidebar_item_content">
    <p><strong>Member Blogs</strong></p>
    [% FOREACH member IN sorted_members %]
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', member.bid) %]">[% member.name %]</a></p>
    [% END %]
  </div>
</div>

After adding the new sidebar menu we have:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Recent Blog Entries</strong></p>
        [% FOREACH blog_entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
          <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
          <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Member Blogs</strong></p>
        [% FOREACH member IN sorted_members %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', member.bid) %]">[% member.name %]</a></p>
        [% END %]
      </div>
    </div>

  </div>

  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p> 
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Run it and you should get a functional, decent looking sidebar menu with links to the most recent blog entries of all members:

yfc11f1.png

Summary

Yardbird/lib/Yardbird.pm

Configure to use AutoCRUD in the application class:

use Catalyst qw/
  -Debug
  ConfigLoader
  Static::Simple

  AutoCRUD

  Authentication
  Authorization::Roles

  Session
  Session::Store::File
  Session::State::Cookie 
/;

Use AutoCRUD when you run the application by going here:

http://0.0.0.0:3000/autocrud

Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm

package Yardbird::Schema::ResultSet::Blog;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';

sub specific {
  my ($self, $bID) = @_;

  return $self->search({id => $bID});
} 

sub most_recent {
  my ($self) = @_;

  return $self->search({}, {order_by => {-desc => ['id']}, rows => 4});
} 

sub all_user {
  my ($self, $uID) = @_;

  return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
} 

1;

Yardbird/lib/Yardbird/Controller/Blog.pm

package Yardbird::Controller::Blog;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller'; }

sub index :Path :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);

  my @members;
  for my $user ($c->model('DB::User')->all) {
    my $blog_entry = $c->model('DB::Blog')->all_user($user->id)->first;
    push @members, { name => $user->name, bid => $blog_entry->id } if $blog_entry; 
  }
  my @sorted_members = sort { "\L$a->{name}" cmp "\L$b->{name}" } @members; 
  $c->stash(sorted_members => [@sorted_members]);

  $c->detach($c->view("TT")); 
}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/root/src/blog/index.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Recent Blog Entries</strong></p>
        [% FOREACH blog_entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
          <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
          <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Member Blogs</strong></p>
        [% FOREACH member IN sorted_members %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', member.bid) %]">[% member.name %]</a></p>
        [% END %]
      </div>
    </div>

  </div>

  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></p> 
      <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
      <p>[% blog_entry.content %]</p>
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Yardbird/lib/Yardbird/Controller/Blog/Entries.pm

package Yardbird::Controller::Blog::Entries;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller'; }

sub index :Path :Args(1) {
  my ($self, $c, $bID) = @_;

  my $row = $c->model('DB::Blog')->specific($bID)->first;
  $c->stash(blog_entry => $row);
  $c->stash(blog_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);
  $c->detach($c->view("TT")); 
}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/root/src/blog/entries/index.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_entry.userid.name %]
<div id="header">
  <div id="header_title">[% blog_entry.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">
    <div class="sidebar_item">
      <div class="sidebar_item_content">
        [% FOREACH entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', entry.id) %]">[% entry.title %]</a></p>
          <p class="sidebar_item_subtitle">[% entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>
  </div>

  <div id="content_with_sidebar">
    <p class="content_title">[% blog_entry.title %]</p>
    <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
    <p>[% blog_entry.content %]</p>
  </div>
</div>

[% INCLUDE footer.tt %]

Yardbird/root/src/header.tt

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>[% title %]</title>
  <link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/main.css') %]" />
</head>
<body>
  <div id="wrapper">
    <div id="main_menu">
      <ul>
        <div id="main_menu_left">
          <li><a href="[% c.uri_for_action('/index') %]">Home</a></li>
          <li><a href="[% c.uri_for_action('blog/index') %]">Blog</a></li>
          <li>Events</li>
          <li>Links</li>
          <li>News</li>
          <li>Photos</li>
          <li>Video</li>
          <li>Members</li>
        </div>
        <div id="main_menu_right">
          [% IF c.user_exists %] 
            <li>[% c.user.name %] | <a href="[% c.uri_for_action('/logout/index') %]">Logout</a></li>
          [% ELSE %]
            <li><a href="[% c.uri_for_action('/login/index') %]">Login</a> | <a href="[% c.uri_for_action('/member/about') %]">Not a member?</a></li> 
          [% END %]
        </div>  
      </ul>
    </div> <!-- main_menu -->

Yardbird/root/static/css/main.css

#wrapper {
  width: 960px;
  margin-top: 0px;
  margin-right: auto;
  margin-left: auto;
}

#header {
  height: 100px;
  margin-left: 20px;
  background: #004060;
  background-image: url(../images/bird/bird.gif);
  background-position: left bottom;
  background-repeat: no-repeat; 
}

#header_title {
  margin-left: 70px;
  padding-top: 22px;
  color: #87ceeb;
  font-size: 2.2em;
  font-weight: bold;
}

#header_subtitle {
  margin-left: 70px;
  margin-top: 2px;
  color: #87ceeb;
  font-size: 0.9em;
}

#footer {
  padding-top: 0.4em;
  padding-bottom: 1.5em;
  background: #004060;
  color: #FFFFFF;
  font-size: 0.9em;
}

body {
  font-family: arial;
  margin-top: 0px;
  background: #004060;
}

#main_menu {
  border-left: 1px solid #FFFFFF;
  border-right: 1px solid #FFFFFF;
  border-bottom: 1px solid #FFFFFF;
  border-bottom-left-radius: 0.3em; 
  border-bottom-right-radius: 0.3em; 
  background: #003050;
  font-size: 0.9em;

  color: #FFFFFF;
}

#main_menu ul {
  padding-top: 0.275em;
  padding-bottom: 0.18em;
  margin: auto; /* center horizontally */
  list-style: none; 

  /* Menu background/border has no vertical height without
  this, I don't know why it works. */
  overflow: hidden;
}

#main_menu a {
  color: #87ceeb;
  text-decoration: none; 
}

#main_menu a:hover {
  color: #FFFFFF;
  text-decoration: underline; 
}

#main_menu_left li {
  padding-right: 5em;
  /*padding-left: 0em;*/
  margin-left: -1.5em;
  float: left;
  /*width: 1em;*/
}

#main_menu_right li {
  /*padding-right: 0em;*/
  /*padding-left: 40px;*/
  margin-right: 1em;
  float: right;
  /*width: 12.5em;*/
}

#content_wrapper {
  border: 1px solid #000000;
  border-radius: 0.3em; 
  background: #FFFFFF;
  /*background: #A4D3EE;*/

  /* Expand content area when sidebar is vertically longer than content. */
  overflow: auto; 
}

/* formhandler generates html with id selectors named 'content' for it's
 * 'content' fields in the blog and blogcomments forms. For this reason I
 * renamed the 'content' id style 'content_with_sidebar'. */

#content_with_sidebar {
  margin-right: 225px;
  margin-left: 10px;
  margin-top: 0px;
  margin-bottom: 20px;
}

#content_without_sidebar {
  margin-right: 10px;
  margin-left: 10px;
  margin-top: 0px;
  margin-bottom: 20px;
}

#sidebar_wrapper {
  float: right;
}

.sidebar_item {
  width: 200px;
  margin-right: 10px;
  margin-top: 13px;
  margin-bottom: 10px;
  border: 1px solid #000000;
  border-radius: 0.3em; 
  /*background: #FFFFFF;*/
}

.sidebar_item_content {
  padding-left: 12px;
  padding-right: 12px;
}

#user_form_button {
  margin-left: 140px;
}

#user_form .label {
  float: left;
  width: 140px;
} 

#user_form_textarea {
  margin-top: -1.15em;
  margin-left: 140px;
}

.content_title {
  font-size: 1.4em;
  font-weight: bold;
  margin-top: 0.3em;
}

.content_subtitle {
  font-size: 0.8em;
  margin-top: -1.6em;
}

.content_title a {
  color: #004080;
  text-decoration: none; 
}

.content_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

.sidebar_item_title {
  font-size: 0.9em;
  font-weight: bold;
}

.sidebar_item_title a {
  color: #004080;
  text-decoration: none; 
}

.sidebar_item_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

.sidebar_item_subtitle {
  font-size: 0.65em;
  margin-top: -1.5em;
}

.sidebar_item_sub_subtitle {
  font-size: 0.65em;
  margin-top: -1.0em;
}

Leave a comment

About j0e

user-pic I've been studying Catalyst when I can find time and will soon be applying for jobs. My experience is with embedded, realtime systems running DOS, Assembly Language and C. I live in Dover-Foxcroft, Maine but would welcome the opportunity to live anywhere I can work and develop my skills, especially overseas. I live by myself with an Australian Cattle Dog I call "Bird" for the great alto sax genius Charlie "Yardbird" Parker. I am looking forward to attending YAPC::NA in Austin, and hope to meet some of the people who have helped me at irc.perl.org.