Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

cp

  • Loading branch information...
commit 07712fbb4c6986678caa6ca39e12e034a25cec7a 1 parent 23fa3d1
dbsh3517 dbsh3517 authored committed

Showing 1 changed file with 693 additions and 0 deletions. Show diff stats Hide diff stats

  1. +693 0 public_html/tweetylicious.pl
693 public_html/tweetylicious.pl
... ... @@ -0,0 +1,693 @@
  1 +#!/usr/bin/perl
  2 +#==================================================#
  3 +# Tweetylicious! A one-file microblog application #
  4 +#--------------------------------------------------#
  5 +# this file is meant as an example of how easy it #
  6 +# is to create cool web applications using cutting #
  7 +# edge technology, with Perl 5, JavaScript and #
  8 +# just a few lines of code! #
  9 +# #
  10 +# Tweetylicious is meant as a hommage to Twitter, #
  11 +# a very cool micro-blogging site, but is in no #
  12 +# way affiliated with it. We hope this work, which #
  13 +# is released for free as open source software #
  14 +# (see LICENSE in the bottom), will stimulate #
  15 +# all the young minds out there to create even #
  16 +# more amazing stuff. Viva la revolution! :) #
  17 +#==================================================#
  18 +
  19 +#--------------------------------------#
  20 +# first we create our database (model) #
  21 +#--------------------------------------#
  22 +package Model;
  23 +
  24 +use ORLite {
  25 + file => 'tweetylicious.db',
  26 + cleanup => 'VACUUM',
  27 + create => sub {
  28 + my $dbh = shift;
  29 + $dbh->do('CREATE TABLE user (username TEXT NOT NULL UNIQUE PRIMARY KEY,
  30 + password TEXT NOT NULL,
  31 + email TEXT,
  32 + gravatar TEXT,
  33 + bio TEXT
  34 + );'
  35 + );
  36 + $dbh->do('CREATE TABLE post (id INTEGER NOT NULL PRIMARY KEY
  37 + ASC AUTOINCREMENT,
  38 + username TEXT NOT NULL
  39 + CONSTRAINT fk_user_username
  40 + REFERENCES user(username)
  41 + ON DELETE CASCADE,
  42 + content TEXT NOT NULL,
  43 + date INTEGER NOT NULL);'
  44 + );
  45 + $dbh->do('CREATE TABLE follow (id INTEGER NOT NULL PRIMARY KEY
  46 + ASC AUTOINCREMENT,
  47 + source TEXT NOT NULL
  48 + CONSTRAINT fk_user_username
  49 + REFERENCES user(username)
  50 + ON DELETE CASCADE,
  51 + destination TEXT NOT NULL);'
  52 + );
  53 + },
  54 +};
  55 +
  56 +
  57 +# this returns who follows our user.
  58 +# Each element is a hash of usernames and gravatars
  59 +sub get_followers_for {
  60 + return Model->selectall_hashref(
  61 + 'SELECT username, gravatar FROM user, follow
  62 + WHERE user.username = follow.source
  63 + AND follow.destination = ?',
  64 + 'username', {} , $_[0],
  65 + );
  66 +}
  67 +
  68 +
  69 +# this returns who our user follows
  70 +sub get_followed_by {
  71 + return Model->selectall_hashref(
  72 + 'select username, gravatar from user, follow
  73 + where user.username = follow.destination
  74 + and follow.source = ?',
  75 + 'username', {}, $_[0],
  76 + );
  77 +}
  78 +
  79 +
  80 +# this returns our search results
  81 +sub search_posts {
  82 + my @items_to_search = @_;
  83 + my $query = 'OR post.content LIKE ? ' x (@items_to_search - 1);
  84 + return Model->selectall_arrayref(
  85 + "SELECT user.username, post.id, gravatar, content,
  86 + datetime(date, 'unixepoch', 'localtime') as date
  87 + FROM user
  88 + LEFT JOIN post ON user.username = post.username
  89 + WHERE post.content LIKE ? $query
  90 + ORDER BY date DESC",
  91 + { Slice => {} }, map { "%$_%" } @items_to_search
  92 + );
  93 +}
  94 +
  95 +
  96 +# this returns sorted posts from all users in @users
  97 +sub fetch_posts_by {
  98 + my @users = @_;
  99 + my $query = 'OR post.username = ? ' x (@users - 1);
  100 + return Model->selectall_arrayref(
  101 + "SELECT user.username, post.id, gravatar, content,
  102 + datetime(date, 'unixepoch', 'localtime') as date
  103 + FROM user
  104 + LEFT JOIN post ON user.username = post.username
  105 + WHERE post.username = ? $query
  106 + ORDER BY date DESC",
  107 + { Slice => {} }, @users
  108 + );
  109 +}
  110 +
  111 +
  112 +# this validates registration data before we commit to the database
  113 +sub validate {
  114 + my ($user, $pass, $pass2, $routes) = @_;
  115 + return 'username field must not be blank' unless $user and length $user;
  116 + return 'password field must not be blank' unless $pass and length $pass;
  117 + return 'please re-type your password' unless $pass2 and length $pass2;
  118 + return "passwords don't match" unless $pass eq $pass2;
  119 + return 'sorry, this user already exists'
  120 + if Model::User->count( 'WHERE username = ?', $user) > 0;
  121 +
  122 + # let's not allow usernames that are part of a valid route
  123 + return 'sorry, invalid username'
  124 + if grep { length $_->name and index($user, $_->name) == 0 } @$routes;
  125 +
  126 + return;
  127 +}
  128 +
  129 +
  130 +#-------------------------#
  131 +# now the web application #
  132 +#-------------------------#
  133 +package main;
  134 +
  135 +use Mojolicious::Lite;
  136 +use Mojo::ByteStream 'b'; # for unicode and md5
  137 +use POSIX qw(strftime);
  138 +
  139 +# this is a fake static route for our static data (static.js, static.css)
  140 +get '/static' => 'static';
  141 +
  142 +
  143 +# this controls the main index page
  144 +get '/' => 'index';
  145 +
  146 +
  147 +# search!
  148 +get '/search' => sub {
  149 + my $self = shift;
  150 + my @items = split ' ', $self->param('query');
  151 +
  152 + $self->stash( post_results => Model::search_posts(@items) );
  153 +} => 'search';
  154 +
  155 +
  156 +# these two control a user registering
  157 +get '/join' => 'join';
  158 +post '/join' => sub {
  159 + my $self = shift;
  160 + my $user = $self->param('username');
  161 + my $error = Model::validate( $user, $self->param('pwd'), $self->param('re-pwd'), app->routes->children);
  162 + $self->stash( error => $error );
  163 + return if $error;
  164 +
  165 + Model::User->create(
  166 + username => $user,
  167 + password => b(app->secret . $self->param('pwd'))->md5_sum,
  168 + email => $self->param('email'),
  169 + gravatar => b($self->param('email'))->md5_sum,
  170 + bio => $self->param('bio'),
  171 + );
  172 +
  173 + # auto-login the user after he joins, and show his/her homepage
  174 + $self->session( name => $user );
  175 + $self->redirect_to("/$user");
  176 +} => 'join';
  177 +
  178 +
  179 +# user login
  180 +get '/login' => 'login';
  181 +post '/login' => sub {
  182 + my $self = shift;
  183 + my $user = $self->param('username') || '';
  184 +
  185 + if ( Model::User->count( 'WHERE username=? AND password=?',
  186 + $user, b(app->secret . $self->param('password'))->md5_sum) == 1
  187 + ) {
  188 + $self->session( name => $user );
  189 + return $self->redirect_to("/$user");
  190 + }
  191 + $self->stash( error => 1 );
  192 +} => 'login';
  193 +
  194 +
  195 +# user logout is just a matter of expiring the session
  196 +get '/logout' => sub {
  197 + my $self = shift;
  198 + $self->session( expires => 1);
  199 + $self->redirect_to('/');
  200 +};
  201 +
  202 +
  203 +# this controls a user's page
  204 +get '/(.user)' => sub {
  205 + my $self = shift;
  206 + my $user = $self->param('user');
  207 +
  208 + # renders our error page unless the user exists
  209 + return $self->render('not_found')
  210 + unless Model::User->count('WHERE username = ?', $user);
  211 +
  212 + # who this user is following?
  213 + my $following = Model::get_followed_by($user);
  214 +
  215 + # fetch posts by user and, if the user is looking at its own page,
  216 + # show posts from people he/she is following too!
  217 + my @targets = ( $user );
  218 + if ($self->session('name') and $self->session('name') eq $user) {
  219 + push @targets, keys %$following;
  220 + }
  221 + my $posts = Model::fetch_posts_by(@targets);
  222 +
  223 + # check if this user is already followed by our visitor,
  224 + # so we display the appropriate "follow/unfollow" link
  225 + if ( $self->session('name')
  226 + and Model::Follow->count('WHERE source = ? AND destination = ?',
  227 + $self->session('name'), $user)
  228 + ) { $self->stash(followed => 1) }
  229 +
  230 + # fill our stash with information for the template
  231 + $self->stash(
  232 + user => Model::User->load( $user ),
  233 + posts => $posts || [],
  234 + followers => Model::get_followers_for($user),
  235 + following => $following,
  236 + total_posts => Model::Post->count('WHERE username = ?', $user),
  237 + );
  238 +} => 'homepage';
  239 +
  240 +
  241 +# The rest of the routes are specific to logged in users, so we
  242 +# add a ladder to make sure (instead of making sure inside each route)
  243 +ladder sub {
  244 + my $self = shift;
  245 + return 1 if $self->session('name');
  246 + $self->redirect_to('/login') and return;
  247 +};
  248 +
  249 +
  250 +# user wants to follow another
  251 +get '/(.user)/follow' => sub {
  252 + my $self = shift;
  253 + my ($source, $target) = ($self->session('name'), $self->param('user'));
  254 +
  255 + Model::Follow->create(source => $source, destination => $target);
  256 + $self->redirect_to("/$target");
  257 +};
  258 +
  259 +
  260 +# user doesn't want to follow anymore
  261 +get '/(.user)/unfollow' => sub {
  262 + my $self = shift;
  263 + my ($source, $target) = ($self->session('name'), $self->param('user'));
  264 + Model::Follow->delete('WHERE source = ? AND destination = ?', $source, $target);
  265 + $self->redirect_to("/$target");
  266 +};
  267 +
  268 +
  269 +# next comes actions that can only be performed if the user is
  270 +# looking at its own posts (creating and deleting posts),
  271 +# so we do another ladder
  272 +ladder sub {
  273 + my $self = shift;
  274 + $self->redirect_to('/')
  275 + unless $self->session('name') eq $self->param('user');
  276 +};
  277 +
  278 +
  279 +# this one handles users creating new posts ('message')
  280 +post '/(.user)/post' => sub {
  281 + my $self = shift;
  282 + my $user = $self->session('name');
  283 +
  284 + if( $self->param('message') ) {
  285 + my $post = Model::Post->create(
  286 + username => $user,
  287 + content => $self->param('message'),
  288 + date => time,
  289 + );
  290 +
  291 + # if it's an Ajax request, return a JSON object of post and gravatar
  292 + my $header = $self->req->headers->header('X-Requested-With') || '';
  293 + if ($header eq 'XMLHttpRequest') {
  294 + $post->{date} = strftime "%Y-%m-%d %H:%M:%S", localtime($post->{date});
  295 + my $gravatar = Model::User->load($user)->gravatar;
  296 + return $self->render_json({ %$post, gravatar => $gravatar });
  297 + }
  298 + }
  299 +
  300 + # otherwise, just render the user page again
  301 + $self->redirect_to("/$user");
  302 +};
  303 +
  304 +
  305 +get '/(.user)/post/:id/delete' => sub {
  306 + my $self = shift;
  307 +
  308 + my $post = Model::Post->select('WHERE id = ?', $self->param('id'));
  309 + $post->[0]->delete if $post->[0];
  310 +
  311 + # if it was an Ajax request, we return a JSON object in confirmation
  312 + my $header = $self->req->headers->header('X-Requested-With') || '';
  313 + if ($header eq 'XMLHttpRequest') {
  314 + return $self->render_json( {answer => 1} );
  315 + }
  316 +
  317 + # otherwise, just render the user page again
  318 + $self->redirect_to('/' . $self->session('name'));
  319 +};
  320 +
  321 +
  322 +# let's rock and roll!
  323 +app->start;
  324 +
  325 +
  326 +#------------------------#
  327 +# finally, the templates #
  328 +#------------------------#
  329 +__DATA__
  330 +@@ layouts/main.html.ep
  331 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  332 +<html xmlns="http://www.w3.org/1999/xhtml">
  333 + <head>
  334 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  335 + <title>Tweetylicious</title>
  336 + <link type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/dark-hive/jquery-ui.css" rel="Stylesheet" />
  337 + <link type="text/css" rel="stylesheet" media="screen" href="/static.css" rel="Stylesheet" />
  338 + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
  339 + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
  340 + <script type="text/javascript" src="/static.js"></script>
  341 + </head>
  342 + <body>
  343 + <div id="header"><a href="/"><div id="logo">Tweetylicious!</div></a>
  344 + <div class="options">
  345 +% if (session 'name') {
  346 + <a href="/<%= session 'name' %>">Home</a><a href="/logout">Sign-Out</a>
  347 +% } else {
  348 + <a href="/login">Sign-In</a><a href="/join">Join us!</a>
  349 +% }
  350 + </div>
  351 + <div class="search ui-widget">
  352 + <form action="/search" method="GET">
  353 + <input id="search" name="query" type="text" value="" /><input type="submit" value=">" />
  354 + </form>
  355 + </div>
  356 + </div>
  357 +
  358 + <%= content %>
  359 +
  360 + <div id="footer" class="ui-corner-all">Tweetylicious is Powered by <a href="http://perl.org">Perl 5</a>, <a href="http://mojolicious.org">Mojolicious</a>, <a href="http://search.cpan.org/perldoc?ORLite">ORLite</a> and <a href="http://jquery.org">jQuery</a>! Released under <a href="http://dev.perl.org/licenses/">the same terms as Perl itself</a>. </div>
  361 + </body>
  362 +</html>
  363 +
  364 +@@ homepage.html.ep
  365 +% layout 'main';
  366 +% use Mojo::ByteStream 'b';
  367 +<div id="content" class="half ui-corner-left">
  368 +% if (session('name') and session('name') eq $user->{username}) {
  369 + <h2>Hi, <%= session 'name' %>!</h2>
  370 + <form id="post" action="<%= url_for %>/post" method="POST">
  371 + <textarea class="ui-corner-all" cols="50" rows="3" id="message" name="message" tabindex="1"></textarea>
  372 + <span id="charsleft"></span>
  373 + <input id="submit" tabindex="2" type="submit" value="Tell the World!" />
  374 + </form>
  375 +% } else {
  376 +% if ( stash 'followed' ) {
  377 + <a class="fineprint" href="<%= url_for %>/unfollow">[-] unfollow</a>
  378 +% } else {
  379 + <a class="fineprint" href="<%= url_for %>/follow">[+] follow!</a>
  380 +% }
  381 +<h2 id="title"><%= $user->{username} %>'s posts</h2>
  382 +% }
  383 +<ul class="messages">
  384 +%# now we render all the posts in the page
  385 +% foreach my $post ( @$posts ) {
  386 + <li class="ui-corner-all">
  387 +%# the author of the post can delete it
  388 +% if ($post->{username} eq session('name') ) {
  389 + <a href="/<%= $post->{username} %>/post/<%= $post->{id} %>/delete" class="ui-icon ui-icon-trash" title="delete this post"></a>
  390 +% }
  391 + <a class="who" href="/<%= $post->{username} %>"><img src="http://www.gravatar.com/avatar/<%= $post->{gravatar} %>?s=60.jpg" /><%= $post->{username} %></a><span class="what"><%= b($post->{content})->decode('UTF-8')->to_string %></span><span class="when"><%= $post->{date} %></span></li>
  392 +% }
  393 +</ul>
  394 +</div>
  395 +<div id="sub-section" class="ui-corner-right">
  396 + <ul id="bio">
  397 + <li><span>Name</span><%= $user->{username} %></li>
  398 + <li><span>Bio</span><%= $user->{bio} %></li>
  399 + </ul>
  400 + <ul id="followers">
  401 + <li><span><%= scalar keys %$followers %></span> Followers</li>
  402 +% foreach my $face ( keys %$followers ) {
  403 + <li><a href="/<%= $face %>"><img src="http://www.gravatar.com/avatar/<%= $followers->{$face}->{gravatar} %>?s=20.jpg" /></a></li>
  404 +% }
  405 + </ul>
  406 + <ul id="following">
  407 + <li><span><%= scalar keys %$following %></span> Following</li>
  408 +% foreach my $face ( keys %$following ) {
  409 + <li><a href="/<%= $face %>"><img src="http://www.gravatar.com/avatar/<%= $following->{$face}->{gravatar} %>?s=20.jpg" /></a></li>
  410 +% }
  411 + </ul>
  412 + <div id="totalposts"><span><%= $total_posts %></span> Posts</div>
  413 +</div>
  414 +
  415 +@@ login.html.ep
  416 +% layout 'main';
  417 +<div id="content" class="full ui-corner-all">
  418 +<h1>Sign-in</h1>
  419 +% if ( stash 'error' ) {
  420 + <div class="ui-state-error ui-corner-all" style="width:466px">
  421 + <span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em"></span><strong>Sorry, invalid username/password combination.</strong>
  422 + </div>
  423 + <p>Not a user yet? <a href="/join">Join now! It's free!</a></p>
  424 + <hr />
  425 +% }
  426 +<form name="login" method="POST" action="/login">
  427 + <table>
  428 + <tr><td>User name:</td><td><input type="text" tabindex="1" name="username" value="<%= param 'username' %>" /></td></tr>
  429 + <tr><td>Password:</td><td><input type="password" tabindex="2" name="password" value="<%= param 'password'%>" /></td></tr>
  430 + </table>
  431 +<input tabindex="3" type="submit" value="Login!"/>
  432 +</form>
  433 +</div>
  434 +
  435 +@@ join.html.ep
  436 +% layout 'main';
  437 +<div id="content" class="full ui-corner-all">
  438 +<h1>Join us, it's free!</h1>
  439 +% if (my $error = stash 'error') {
  440 + <div class="ui-state-error ui-corner-all" style="width:450px">
  441 + <span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em"></span><strong>Sorry:</strong> <%= $error %>
  442 + </div>
  443 + <hr />
  444 +% }
  445 +<form name="join" method="POST">
  446 + <table>
  447 + <tr><td>Username</td><td><input name="username" type="text" tabindex="1" value="<%= param 'username' %>" /></td></tr>
  448 + <tr><td>Password</td><td><input name="pwd" type="password" tabindex="2" value="<%= param 'pwd' %>" /></td></tr>
  449 + <tr><td>Password (again)</td><td><input name="re-pwd" type="password" tabindex="3" value="<%= param 're-pwd' %>" /></td></tr>
  450 + <tr><td>Email</td><td><input name="email" type="text" tabindex="4" value="<%= param 'email' %>" /></td></tr>
  451 + </table>
  452 + <span class="fineprint">Email is optional, and doesn't show in your page. It's used only to fetch your <a href="http://gravatar.com">gravatar</a></span>
  453 + <p>Tell us a bit about yourself - everyone will see it on your page</p>
  454 + <textarea class="ui-corner-all" tabindex="5" cols="50" rows="3" id="message" name="bio"><%= param 'bio' %></textarea>
  455 + <input type="submit" tabindex="6" value="Create!" />
  456 +</form>
  457 +</div>
  458 +
  459 +@@ search.html.ep
  460 +% layout 'main';
  461 +% use Mojo::ByteStream 'b';
  462 +<div id="content" class="full ui-corner-all" style="text-align:left">
  463 +<h1>Results for '<%= param 'query' %>'</h1>
  464 +
  465 + <ul class="messages">
  466 +% foreach my $post (@$post_results) {
  467 + <li class="ui-corner-all">
  468 +%# the author of the post can delete it
  469 +% if ($post->{username} eq session('name') ) {
  470 + <a href="/<%= $post->{username} %>/post/<%= $post->{id} %>/delete" class="ui-icon ui-icon-trash" title="delete this post"></a>
  471 +% }
  472 + <a class="who" href="/<%= $post->{username} %>"><img src="http://www.gravatar.com/avatar/<%= $post->{gravatar} %>?s=60.jpg" /><%= $post->{username} %></a><span class="what"><%= b($post->{content})->decode('UTF-8')->to_string %></span><span class="when"><%= $post->{date} %></span></li>
  473 +% }
  474 + </ul>
  475 +</div>
  476 +
  477 +@@ index.html.ep
  478 +% layout 'main';
  479 +<div id="content" class="info full ui-corner-all">
  480 +
  481 +<h1>What is Tweetylicious?</h1>
  482 +<p>Tweetylicious is a <a href="http://en.wikipedia.org/wiki/Micro-blogging">microblogging</a> web application in a single file! It was built from scratch using state of the art technology, and is meant to demonstrate how easy and fun it is to create your own Web applications in modern Perl 5!</p>
  483 +
  484 +<h1>What are its features?</h1>
  485 +<ul>
  486 + <li>Multi-user, with homepages, search and list of followers/following</li>
  487 + <li>Nice, clean, pretty interface (at least I think so :P)</li>
  488 + <li>User avatar images provided by <a href="http://gravatar.com">gravatar</a></li>
  489 + <li>Unicode support</li>
  490 + <li>Well structured, commented code, easy to expand and customize</li>
  491 + <li>Encrypted online sessions</li>
  492 + <li>Uses an actual database (SQLite) and stores encrypted user password</li>
  493 + <li>Free and Open Source Software, released under the same terms as Perl itself.</li>
  494 +</ul>
  495 +
  496 +<h1>How can you fit all that in a 'single file'?! It's gotta be huge and clobbered then!</h1>
  497 +<p>Not at all! Tweetylicious is built on top of Mojolicious::Lite and ORLite, two very simple modules that have absolutely no dependency other than Perl 5 itself. Mojolicious::Lite allows you to create powerful web applications in a very simple and clean fashion, while also letting you integrate your templates on the bottom of the file. ORLite is an extremely lightweight ORM for <a href="http://sqlite.org">SQLite</a> databases that lets you specify your schema on the fly.</p>
  498 +<p>Removing just blank lines and comments, the Model has ~80 lines, the Controller ~110 lines, templates ~170 lines, plus ~90 lines of static css and ~60 of static javascript. And that's the <strong>whole</strong> app.</p>
  499 +<p>But don't take my word for it, just browse through it :)</p>
  500 +
  501 +<h1>What do I need to make it work on my own system?</h1>
  502 +
  503 +<ul>
  504 + <li>Perl 5 <span class="fineprint">(if you're running Linux or Mac, you already have it! Windows users can get it <a href="http://strawberryperl.com">here</a>)<span></li>
  505 + <li>SQLite 3</li>
  506 + <li>Mojolicious</li>
  507 + <li>ORLite</li>
  508 +</ul>
  509 +
  510 +<p>Tweetylicious also relies on the powerful jQuery JavaScript library, but that's downloaded and processed by the clients browser, so don't worry about it. Each user's avatar image is also provided externally, via gravatar.</p>
  511 +
  512 +<p>Have fun!</p>
  513 +</div>
  514 +
  515 +@@ not_found.html.ep
  516 +% layout 'main';
  517 +<div id="content" class="full ui-corner-all">
  518 +<h3>Sorry, we couldn't find the page you were looking for :-(</h3>
  519 +</div>
  520 +
  521 +@@ static.css.ep
  522 + body {
  523 + width:720px;
  524 + margin:0 auto;
  525 + text-align:center;
  526 + background: #0f1923; /* #333; */
  527 + border:0;
  528 + }
  529 + a { text-decoration: none }
  530 +
  531 + #header,#content,#sub-section,#footer {
  532 + overflow:hidden;
  533 + display:inline-block;
  534 + text-align:left
  535 + }
  536 + #header li { display: inline }
  537 + #logo {
  538 + float: left;
  539 + background: #0972a5;
  540 + height: 60px;
  541 + font-family: "Georgia", "Times New Roman", serif;
  542 + font-size: 26px;
  543 + color: #eee;
  544 + padding: 50px 10px 10px 10px;
  545 + }
  546 + .options { text-align: right; margin-left: 450px; margin-top: -5px }
  547 + .search { float: right; margin-top: 50px }
  548 + #search { background-color: #bbb; color: #444; width: 200px; font-size: 16px; }
  549 + #content {
  550 + background: #efe;
  551 + font-family: "Verdana", sans-serif;
  552 + min-height: 100px;
  553 + padding-left: 10px;
  554 + }
  555 + h1 { font-size: 1.2em }
  556 + #title { margin-left: 30px; }
  557 + .half { width: 78.7% }
  558 + .full { width: 100% }
  559 + .fineprint { font-size: 0.6em }
  560 + ul { margin: 0; padding: 0; list-style: none; list-style-position: outside; }
  561 + .info { padding-bottom: 10px }
  562 + .info ul { list-style-type: square}
  563 + .info ul li{ margin-bottom:10px }
  564 + #content ul {
  565 + display: block;
  566 + width: 90%;
  567 + margin: 10px auto;
  568 + }
  569 + #content ul.messages li {
  570 + border-top: 1px solid #ddd;
  571 + padding-top: 16px;
  572 + height: 70px;
  573 + margin-top: 10px;
  574 + }
  575 + .when { display: block; font-size: 10px; color: #aaa; }
  576 + img { float: left; margin: 1px; border: 0 }
  577 + #content .ui-icon { float: right; position: relative; top: -10px; right: 10px }
  578 + #content a:hover.ui-icon { border: 1px #ff0 dashed }
  579 + #content a { text-decoration: none }
  580 + .who { margin-right: 8px; font-weight: bold }
  581 + #sub-section {
  582 + width: 20%;
  583 + background: #ccc;
  584 + font: 0.8em "Verdana", sans-serif;
  585 + }
  586 + #message {
  587 + border: 1px solid #aaa;
  588 + padding: 4px 2px;
  589 + resize: none;
  590 + font-size: 1.15em;
  591 + font-family: sans-serif;
  592 + color: #333;
  593 + }
  594 + #post {
  595 + margin: 10px 50px 30px 50px;
  596 + }
  597 + #post input { margin-right: 54px; float: right; font-size: 0.6em; }
  598 + #charsleft {
  599 + display: block;
  600 + float: left;
  601 + font-weight: bold;
  602 + }
  603 + .orange { color: #ff6300 }
  604 + .red { color: #d11 }
  605 + #bio li { margin: 6px; line-height: 1em; }
  606 + #bio span, #followers span, #following span, #totalposts span {
  607 + font-weight: bold;
  608 + margin-right: 4px;
  609 + }
  610 + #followers li, #following li { margin: 1px }
  611 + #followers, #following, #totalposts { clear: both; margin-left: 5px; padding-top: 10px }
  612 + /* safari and opera need this */
  613 + #header,#footer {width:100%}
  614 +
  615 + #content,#sub-section {float:left; margin-top: 20px; min-height: 360px; }
  616 + #footer {clear:left; margin: 20px auto; padding-top: 10px;height: 26px; background: #555; color: #ccc; font-size:12px; text-align: center; }
  617 + #footer a { text-decoration: none; color: #eee }
  618 +
  619 +@@ static.js.ep
  620 +$(function() {
  621 + // creating our buttons
  622 + $(".options").find("a").button();
  623 + $("#submit").button();
  624 +
  625 + // search bar effects
  626 + var searchDefault = "Search Tweetylicious...";
  627 + $("#search").val(searchDefault);
  628 + $("#search").focus( function() {
  629 + if($(this).val() == searchDefault) $(this).val("");
  630 + });
  631 + $("#search").blur(function(){
  632 + if($(this).val() == "") $(this).val(searchDefault);
  633 + });
  634 +
  635 + // showing how many characters are left
  636 + $("#charsleft").text("140 characters left");
  637 + $("#message").keyup(function() {
  638 + var left = 140 - $("#message").val().length;
  639 + if (left < 0 ) {
  640 + $("#charsleft").removeClass("orange").addClass("red");
  641 + $("#submit").button("option", "disabled", true);
  642 + } else {
  643 + $("#submit").button("option", "disabled", false);
  644 + if (left < 40) {
  645 + $("#charsleft").removeClass("red").addClass("orange");
  646 + } else {
  647 + $("#charsleft").removeClass("red").removeClass("orange");
  648 + }
  649 + }
  650 + $("#charsleft").text( left + ' characters left' );
  651 + });
  652 +
  653 + // highlighting selection
  654 + $("#content ul.messages li").hover(
  655 + function() { $(this).animate( {backgroundColor:'#ded'}, 400 ); },
  656 + function() { $(this).animate( {backgroundColor:'#efe'}, 400 ); }
  657 + );
  658 +
  659 + /* if user has javascript enabled, we turn
  660 + 'delete post' and 'tell the world' buttons into Ajax
  661 + (well, actually Ajaj, since we use JSON ;) */
  662 + function send_to_trash(event) {
  663 + event.preventDefault();
  664 + var item = this;
  665 + var href = $(item).attr("href");
  666 + $.getJSON(href, function(json) {
  667 + if (json.answer) {
  668 + $(item).parent("li").hide("explode", {}, 1000);
  669 + }
  670 + });
  671 + }
  672 + $("a.ui-icon").click(send_to_trash);
  673 +
  674 + $("#submit").click(function(event) {
  675 + event.preventDefault();
  676 + var href = $("#post").attr("action");
  677 + $.post(href, $("#post").serialize(), function(data) {
  678 + $("#message").text("");
  679 + $("#content ul").prepend('<li style="display:none" class="ui-corner-all"><a href="/' + data.username + '/post/' + data.id + '/delete" class="ui-icon ui-icon-trash" title="delete this post"></a><a class="who" href="/' + data.username + '"><img src="http://www.gravatar.com/avatar/' + data.gravatar + '?s=60.jpg" />' + data.username + '</a><span class="what">' + data.content + '</span><span class="when">' + data.date + '</span></li>');
  680 + $("#content li:first").show("drop", {}, 1000);
  681 + $("#content li:first").find("a.ui-icon").click(send_to_trash);
  682 + }, "json");
  683 + });
  684 +
  685 + // formatting our content
  686 + $(".what").each(function() {
  687 + var message = $(this).html()
  688 + .replace(/@(\w+)/g, "@<a href=\"/$1\">$1</a>")
  689 + .replace(/#(\w+)/g, "<a href=\"/search?query=%23$1\">#$1</a>");
  690 + $(this).html(message);
  691 + });
  692 +});
  693 +

0 comments on commit 07712fb

Please sign in to comment.
Something went wrong with that request. Please try again.