Code, Crafts, and Craic: The NextJen Mobile Blog

My friend Jenny operates as NextJen Mobile, which led to me creating a NextJen logo last fall.

Recently, Jenny decided to pull her development blog – Code, Crafts, and Craic – off of Blogger and onto the NextJen site.  I ended up coming up with a type treatment that ties Code, Crafts, and Craic into the NextJen branding.

The important question to answer first was whether Code, Crafts, and Craic was becoming the NextJen blog or whether the NextJen blog was called Code, Crafts, and Craic.  It may sound similar but it’s a matter of which brand is more important.  Jenny decided that the blog would be keeping it’s name and focus, it was just moving inside the NextJen web property.

With that decided, we knew that the focus was on making the existing name look like NextJen, rather than tacking the existing name onto NextJen.

nextjen-ccc

We started with the “NextJen” type treatment and applied it to the words “code,” “crafts,” and “craic.”  The word “and” is dropped in the empty space between “crafts” and “craic” but the commas in the name are left implied by line breaks.  The three Cs are aligned to the left but the block of text that forms is centered over the text “The NextJen Mobile Blog.”

The aerials that appear over the tittle of the J in the NextJen logo are repeated over the O in code, modified to fit the O better.  The aerials are derivative in both logos but it provides an extra bit of visual continuity tying them together.

Trello as an Interface via Webhooks

So I’m a bit particular about how I keep my finances in order.  To the point that I wrote my own web-based ledger software to help myself keep it all straight.  Yeah, there are third-party solutions out there, but I wanted something that worked exactly the way I wanted.

Every couple days I pull receipts out of my pocket and go through my email inbox and drop new entries in my ledger.  Every couple weeks I manually reconcile the ledger with my banking statements.  Manual processes – ew – but it’s important enough to me that I do it.

The issue I ran into was transactions that I didn’t have a receipt for.  Tim Horton’s drive-thru or gas station pumps with a broken receipt printer or the Flint Firebirds’ souvenir shop using SquareCash.  No receipt means no ledger entry means confusion when I go to reconcile.

I could have made a mobile version of the ledger entry form but I really didn’t want to. As such I decided I could fix the issue by keeping faux-receipts electronically in Trello.  A single list.  Card title is the place I spent the money, description is the amount, a label to represent the account.  Once there’s a ledger entry, archive the card.

And that would have been enough.  I decided to take it a step further and automate things.

I use a webhook subscribed to that single list to look for new cards, get the data from them, automatically add the record to my ledger, then archive the card. I’m essentially using Trello’s app as an interface for my own so that I don’t have to make a mobile interface.

It’s a bit hacky but I figured I’d throw some of the code out here since I feel like there’s not a lot of Trello webhook documentation out there.  Unlike my usual, I’m going to redact some of the code as it deals with my financial system and I’d prefer not to put that out there.

<?php
  require_once 'trello_api.php';
  $trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

  $data = json_decode(file_get_contents('php://input'));

  if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
    // CREATE WEBHOOK ON CARD
    $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
    $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
  }

  if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
    // ASSIGN VARIABLES
    $date = date('Y-m-d', strtotime($data->action->date));
    $label = trim($data->model->name);
    $amt = trim(str_replace('$', '', $data->model->desc));

    $category = get_category_id($label);
    $account_id = get_account_name($data->model->labels[0]->name);

    $details = array(array('category_id' => $category, 'amt' => $amt));

    if (($account_id) AND ($category) AND (is_numeric($amt))) {
      // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
      $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

      $webhook_id = 0;
      foreach ($comments AS $comment) {
        if (strstr($comment->data->text, 'Webhook ID: ')) {
          $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
          break;
        }
      }

      if ($webhook_id) {
        // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
        $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
        $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
        create_ledger_entry($account_id, $date, $label, $amt, $details);
      } else {
        // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
        $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
      }
    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

As I said, I’ve replaced some of the actual code with obscured-away psudo-functions. I’ll point those out those spots as necessary while going through this piece-by-piece.

require_once 'trello_api.php';
$trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

$data = json_decode(file_get_contents('php://input'));

Right off the bat we pull in my Trello API wrapper class (which really could use some love, maybe I’ll get to that sooner or later) and instantiate an object that we’ll use later. Then we pull in the data Trello posted to us via the input stream.

if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
  // CREATE WEBHOOK ON CARD
  $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
  $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
}

Webhooks subscribed to a list don’t give us all the details of cards on the list, so if we detect a card creation and we’re certain we’re getting data from the webhook attached to the list, we add a webhook to the new card. This is done with a POST to /1/webhook passing in a description (which is optional and doesn’t really matter), callbackURL (same as the URL of this script, though obscured here), and idModel (the ID of the card). For future reference, we then post the ID of the newly-created webhook to a comment on the card via POST to /1/card/<card_id>/actions/comments with text set as needed.

if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
  // ASSIGN VARIABLES
  $date = date('Y-m-d', strtotime($data->action->date));
  $label = trim($data->model->name);
  $amt = trim(str_replace('$', '', $data->model->desc));

  $category = get_category_id($label);
  $account_id = get_account_name($data->model->labels[0]->name);

  $details = array(array('category_id' => $category, 'amt' => $amt));

The rest of the code only fires if we’re receiving from the new webhook attached to the card, and if it’s triggered by either a card creation (which should never happen since the webhook won’t have been created yet), card update (which happens when I set the card’s description), or label addition (which is how I define what account the transaction is for). We only care about those actions because they match the ones I take on the front-end.

From there, we use attributes of the card to build the transaction. The transaction date is whatever the action is taking place. The label is the name of the card. The amount comes from the card description. The category is determined based on the label. The account_id is determined based on the first label selected. A single-item, multidimensional array (the actual interface handles more complex data) is assembled from this data.

if (($account_id) AND ($category) AND (is_numeric($amt))) {
  // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
  $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

  $webhook_id = 0;
  foreach ($comments AS $comment) {
    if (strstr($comment->data->text, 'Webhook ID: ')) {
      $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
      break;
    }
  }

If we got an account ID and a category and the amount is numeric, we know we got good data and we’re ready to try to add the record. First, though, we make a GET call to /1/cards/<card_id>/actions with filter set to commentCard and fields set to data, so that we can get all of the comments posted to the card. We loop through them until we find the one where we stored the card webhook ID and then we save that ID off. There’s probably a better way to do this.

if ($webhook_id) {
  // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
  $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
  $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
  create_ledger_entry($account_id, $date, $label, $amt, $details);

If we found a webhook ID (and we always should), we delete the webhook, archive the card, and create the ledger entry. The first is accomplished with a DELETE request to /1/webhook/<webhook_id>. The second is a PUT to /1/card/<card_id>/closed with value set to true.

} else {
  // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
  $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
}

If we didn’t get a webhook ID, something is seriously broken so we log that finding to the card as a comment via a POST to /1/card/<card_id>/actions/comments with text set to “No webhook found.”

    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

Lastly, if we don’t have all of the things we need to make a ledger entry we log that to the card as a comment. Again, that’s done with a POST to /1/card/<card_id>/actions/comments.

As I said, it’s hacky. It gave me a chance to play with Trello webhooks a bit, though, and was a lot of fun. As I use it more, we’ll see what I did wrong.

Exporting from Snagit to Amazon S3: Revisited

One of the first things I wrote about when I started this blog was my workaround solution for exporting from TechSmith Snagit to Amazon S3.  That worked okay for Windows but I’ve started working on Mac significantly more of late and I missed that functionality.  As such, I took another look at options for this since TechSmith itself still hasn’t developed a Snagit to S3 output for either Windows or Mac.

I feel like exporting on Mac shouldn’t be a problem.  There’s no S3 Browser available but you could replace it with s3cmd and do the same thing.  The catch: There’s no Program Output option in Snagit Mac.  That’s right, on Windows you can essentially make your own outputs but on Mac you’re out of luck.

I came up with a workaround, though.  It’s not pretty but it works.  It also works on Windows, but with better options available I’m not sure there’s a reason to use it.

I use ExpanDrive to map my S3 buckets as a local drive.  Then I can save from Snagit straight to the location I want in S3.  That part’s great.  It’s pretty much seamless.  ExpanDrive is a really awesome tool.  Probably too expensive if all you’re using it for is Snagit exporting, but worth taking a look at if you’re working with S3 in other ways.

The problem is you don’t get the uploaded URL out of this.  That’s where it gets hacky.

I wrote a Chrome extension that gets me a list of the last five files uploaded to this particular S3 bucket.  So after saving my file, I have to go to my browser to get its URL.  Extra steps.  The bonus is that I can get the URL any time later.

A view of my Chrome extension, showing the last five files uploaded to my filebox S3 bucket.
A view of my Chrome extension, showing the last five files uploaded to my filebox S3 bucket.

Since the ExpanDrive part of it works out of the box, here’s the breakdown of my Chrome extension.

<?php
  // GET THE DATA
  $s3 = Aws\S3\S3Client::factory(array('key' => $access_key, 'secret' => $secret_key, 'region' => $region));

  $objects = array();

  do {
    $response = $s3->listObjects(array('Bucket' => $bucket_name, 'Marker' => $response->NextMarker));

    foreach ($response['Contents'] AS $item) {
      $objects[] = array('key' => $item['Key'], 'url' => ($bucket_url . $item['Key']), 'timestamp' => strtotime($item['LastModified']));
    }
  } while ($response->NextMarker);

  unset($s3);

  // SORT THE DATA
  usort($objects, function($a, $b) {
    return $a['timestamp'] - $b['timestamp'];
  });

  $display = array();
  while (count($display) < 5) {
    $display[] = array_pop($objects);
  }

  // OUTPUT
  header ('Content-type: application/json');
  echo json_encode($display);
?>

I start with a script on the server side that uses the AWSSDKforPHP2 to read in the files from my filebox, sort by date, and grab the five most recent.  Those five are then spit out as JSON.

To access that file from the Chrome extension, it’s important to include that domain in the permissions section of the manifest.json file.  Also necessary is the clipboardWrite permission.  In addition to the required manifest file, the extension uses a single HTML page, a stylesheet (which I’ll skip here since how it looks doesn’t really matter), and a Javascript file.  There are also some images but I’ll skip those, too.

<!doctype html>
<html>
  <head>
    <title>ClarkRasmussen.com Online Filebox</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="container">
      <div id="branding_icon">
        <img src="img/icon-48.png" alt="icon" />
      </div>
      <div id="main">
        <h1>ClarkRasmussen.com Online Filebox</h1>

        <ul id="content"></ul>
      </div>
    </div>

    <script src="main.js"></script>
  </body>
</html>

The important things here are the UL element with the ID of content and the inclusion of main.js.  The UL will be targeted by our JS for dynamically adding elements.

var site_url = 'http://www.mydomain.com/';

function request_data () {
  var xhr = new XMLHttpRequest();
  xhr.open('get', (site_url + 'path/to/script.php'), true);
  xhr.onload = populate_list;
  xhr.send();
}

function populate_list () {
  var obj = JSON.parse(this.responseText);

  // CLEAR OUT EXISTING ELEMENTS
  var container = document.getElementById('content');
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }

  // ADD NEW ELEMENTS
  for (var n = 0; n < obj.length; n++) {
    var li = document.createElement('li');
    var a = document.createElement('a');

    a.innerHTML = obj[n].key;
    a.setAttribute('href', obj[n].url);
    a.setAttribute('target', '_blank');
    a.onclick = function () {
      copy_to_clipboard(this.getAttribute('href'));
    };

    li.appendChild(a);
    container.appendChild(li);
  }
}

function copy_to_clipboard (text) {
  const input = document.createElement('input');
  input.style.position = 'fixed';
  input.style.opacity = 0;
  input.value = text;
  document.body.appendChild(input);
  input.select();
  document.execCommand('Copy');
  document.body.removeChild(input);
};

window.addEventListener('load', request_data);

The request_data function wraps a call to the PHP script noted above.  Onload of that data, we call populate_list.

The first thing we do in populate_list is parse the text we got from the PHP script into an actual JSON object.  Then we remove any list items we may have in our previously-mentioned UL.  We loop through each of the items in our JSON object and create new elements for them.  Each item gets an LI with an A inside it.  The A has an HREF of the item’s URL and the TARGET is set to _blank so it opens in a new window.  Additionally, we use the copy_to_clipboard method that I grabbed from someone’s GitHub to save that URL to the clipboard, setting it as an onclick event for the A tag.

I’m certain that this could be cleaned up and made more configurable and turned into a publicly-available extension but I’m not going to bother with it.  I figured I’d put this out and hope that it helps someone.

I will say that one idea I’m intrigued by is replacing the PHP script with an AWS Lambda function that triggers any time the S3 bucket is updated.  I’m not entirely certain how that would work but it seems possible.

Solve it Saturday: New York Islanders

I wrote a piece for the “Fix it Friday” feature over at SportsLogos.Net but they decided not to run it, so I’m posting it here.

The idea of that feature is to take the uniform set of a sports team and point out what’s wrong with it, then propose an alternative.  My focus was the new alternate jersey of the NHL’s New York Islanders.  What follows is slightly edited from my original text, as I don’t need to follow the same format here as I would there.


The new alternate jersey for the New York Islanders.
The new alternate jersey for the New York Islanders.

The New York Islanders unveiled their new alternate jersey two weeks ago. A departure from their blue and orange standard set, the alternate is black and white, mimicking the color scheme of their new landlords, the Brooklyn Nets.

Though statements at the unveiling of this jersey say otherwise, the new Islanders jersey doesn’t tie into the team’s history at all. It could be any team’s jersey. It doesn’t tie into a history of four consecutive Stanley Cups, even with four stripes to represent those wins. It doesn’t share anything with the modernization of that set worn in the late 1990s into the Reebok Edge era. It (thankfully) doesn’t evoke memories of their previous departure from the norm: the Fishstick jerseys.

A "Brooklynified" New Jersey Devils concept. Black, white, three stripes for three Stanley Cups, and a white, stripped-down logo.
A “Brooklynified” New Jersey Devils concept. Black, white, three stripes for three Stanley Cups, and a white, stripped-down logo.

And – again, despite what team executive say – it’s not supposed to.

While this doesn’t in any way say “Islanders,” it screams “Brooklyn” (at least the Brooklyn brand that the Barclays Centre team is trying to build). Minimalist and a little retro, with rounded fonts.

As such, there are two ways to look at this. Is it a successful New York Islanders jersey? Not in the slightest. Is it a successful Brooklyn hockey jersey? Based on the brand they’re trying to build there, I think so. That said, there are still issues with it.

The Good
The minimalist version of the Islanders’ current primary logo – stripped down to just the connected “NY” – works well as an alternate logo for the team, as it did on their Stadium Series jerseys. Additionally the “B-with-stripes” alternate logo that appears on a tag at the hem of the jersey looks good, as does the BKLYN logo on the helmet that makes use of the Y from the primary.

The inclusion of a white hem stripe at the waist of the jersey, breaking up what would otherwise be an all-black body, is a big factor towards making the color choices work for me. Those stripes also being used at the cuffs of the sleeves is a good bit of consistency.

Similarly, the contrasting-color collar helps break things up, though, as I’ll get to below, I don’t think it’s quite enough.

The Bad
The thing that supposedly ties these Brooklyn-themed jerseys back to the New York Islanders franchise is also the thing that was most poorly executed. Four stripes, one representing each of the team’s Stanley Cup Championships. These stripes appear on each arm and on each sock, but they’re far too thin to appear very visible during game play. They get lost. Even when modeled on a white background, they appear more like a musical staff than anything athletic.

Speaking of tying into the past, on a black-and-white jersey the use of orange for the four stripes in the crest logo is jarring. The blue and orange Brooklyn shoulder patch on a black background also has this problem, as do the helmet logo and the hem tag logo.

The hem tag logo is something else I take issue with. Not the logo itself, which I mentioned liking, but the placement. Those hem tags won’t be visible on the ice, they’re just included for the fans wearing them in the concourse.

Finally, I’’m not sold on the number font. It’’s unique across the NHL but it feels very overbearing. It’s a big, bulky font – with just a little bit of rounding that feels like a throw-in.

Fixing-It
brooklyn-islanders-markup

Fixed
brooklyn-islanders-concept

The sleeve and sock stripes are made thicker, making them more visible.  A white shoulder yoke is added, breaking up the solid black look and giving the blue an orange shoulder patch something to pop off of.  A white stripe is added to the side of the pants and the “B” logo is moved to the bottom of that stripe, replacing the redundant “NY” logo.  Orange is swapped out for white.  A more rounded font is used for the numbers.

There’s no undoing the revolutionary change that is going to black and white for the Islanders. What we can do is clean up some of the mistakes they made in the process. Bolder stripes, rounder numbers, and removal of jarring colors gets us that.  It ends up with a retro-modern look that the Brooklyn organization seems to have been going for.