This was FrontendNE 2018 – well, from my POV

Last week I swapped the 18 degress in Berlin with snowy rain in Newcastle, England. Why? To attend the 2018 edition of the FrontendNE conference and I am happy that I did.

The plane taking me back to Berlin

All in all this was a lovely, cozy and well-organised event. As an extra, it had an audience you don’t come across at a lot at other events. The reasons were threefold. A good location – the big hall of a brewery with proper stage audio equipment. A very affordable ticket price. And the loveliness of the organisers with a no-nonsense MC demeanour and not a hint of arrogance.

The crowd at the event

I like single-track events. It means the organisers have to work harder to ensure each slot is a winner for the audience. In this case, the line-up and topics were diverse and there was a lot to take away.

Val Head : Choose Your Animation Adventure

Val is a well-known authority on anything animation on the web. She has authored quite a few books and courses on the topic. And she teaches people to make things move without making your users queasy or drain the battery of their devices. In this talk she explained different techniques to animate on the web. Starting with CSS animations, past vanilla JS and up to animation libraries. This was a very pragmatic talk explaining the pros and cons of each technoloy with warts and all. Val is a very chipper and engaging speaker, and I am happy she thawed out the audience as the first to step up. Looking forward to the video.

Léonie Watson : You’re only supposed to blow the bloody doors off!

Leonie showing off screen readers

Leonie is an accessibility expert giving sensible and actionable advice on how to create accessible interfaces on the web without overshooting the mark. Yes, there is such a thing as adding too much to make your widgets accessible. Often adding a lot of ARIA also means it is your job to make it work with all kind of assistive technology. Something you can avoid by using the appropriate HTML elements and guiding the user. Hence Leonie’s talk, a nod to The Italian Job. Leonie is a superb presenter. It is great to see a visually impaired person wield stage technology and presentation software as if there is nothing much to it. I liked this talk as it fell neither into the “legal accessibility” nor the “everything works if you only use $thing” camps. Instead, Leonie showed that accessibility is like any other thing. It is a matter of looking into what you are doing and trying your best to make it work. Often this means doing less and the simple thing instead of trying to cater to needs you can’t even know about.

Jack Franklin : A red LEGO brick is always red: components on the web

Jack Franklin is a development lead at thread (the company partly responsible for my swanky style). He showed how they made it much easier to maintain and improve this product using a component approach. Web components are nothing new, of course. Making them work and perform in browsers natively is still trickly though. That’s why many component talks are about the framework du jour and opinionated. Jack did a great job not falling into this trap but showed the real benefits of components. For example hot-fixing a display issue with the certainty that you won’t affect the rest of the page. A great, no-nonsense talk about the subject, well worth a watch.

Niels Leenheer : Having fun with WebBluetooth

Oh how I rooted for this talk to work. Niels is a lovely person and oozes having fun playing with technology. That’s why it was grim to see this talk’s tech demos fail at the Halfstack conference in London earlier this year. Niels still managed to make it a good talk, but seeing him lug lots of hardware to an event just to see it all fail because of connectivity issues was grim. In essence, what Niels proves with this talk is that the specification of Bluetooth and Low-End Bluetooth is a terrible mess. And that with borderline self-flagellating reverse engineering you can do fun things with Web Bluetooth. It is a mess with a lack of standards and at times a total disregard for security. But Niels did a lovely job getting us excited anyways. Top tip though: do not fly back with him as airport security doesn’t like his suitcase full of Bluetooth marvels.

Sara Vieira : Your brain does not have a fix flag

Sara explaining that is hard to be normal

Sara’s talk was the big surprise. It wasn’t a tech talk, although she is highly capable of giving those, too. It was instead a no holes barred account of her life story dealing with and overcoming anxiety. A very important talk about a mental health issue that is tough to understand for people and hard to empathise with. I hope that the video of this will do the rounds and inspire people to care more and those affected to find the strength to find help.

Ian Feather : Frontend Resilience

I wished I had seen more of this talk, but I was bricking it as I was up next. Ian works for BuzzFeed and showed the many ways they ensure the site stays up even when everything is against you. Instead of having a “this is how to make your site performant and everything will be rosy” Ian talked shop. CDNs not working like you expect them, data feeds timing out, the whole horror show of network connectivity. I’m looking forward to seeing this.

Chris Heilmann : 7 things you can do to become a happier JavaScript developer

Hated it. Knew all the content. Boring. Also, what’s with that accent?

General conference feedback

Everything worked nicely and people were very well fed and watered. Actually there was a lot of yummy food left over, which was a shame. The timing worked out, the breaks were well-timed. The location was gorgeous with a lovely park outside full of dogs and swans and their interplay.

The after-party was at a location that had pool billiards, minigolf, bowling and many other things. The food was plenty, two vouchers for drinks ensured that people got merry and charming and not annoying. I only used the bowling lane as I had a lot of people come up to me and ask me questions. I heard from witnesses that parts of the sounds in the karaoke room violated the Geneva convention but that may have been hearsay (or what’s left of the hearing).

The self-demeaning jokes of the organisers on stage, “turns out using sketch for print wasn’t the best idea, just imagine those missing letters” showed that this event was a labour of love and not a way to make money. I like when an event outside the usual spaces for events works out that well. I have the same fondness for Beyond Tellerand, as Duesseldorf isn’t a hub of web news either. I am very happy to have contributed to this event in Newcastle and hope that more will come soon.

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Interview with David Braun from TemplateMonster

We had the opportunity to communicate with David Braun (co-founder and CEO of recently. TemplateMonster is a marketplace featuring 46,000 templates for many different types of websites.

Why should Web Professionals care about this?

  • TemplateMonster can save you time
  • Templates exist for major platforms (WordPress, Drupal, and much more)
  • This means you can speed your workflow

David Braun is a co-founder and CEO of This company is the oldest and the most experienced on the market in this area. We invited David to talk and provided some questions for our discussion.

David Braun, co-founder and CEO of

David Braun, co-founder and CEO of

What is from your point of view?

TemplateMonster has become a marketplace now. It features 46,000 pre-designed templates crafted for different types of sites, business niches, and engines including the most popular platforms: WordPress, WooCommerce, Joomla, Magento, Drupal, PrestaShop, and Moto CMS. Our aim is to meet the requirements of as many customers as possible. We offer plenty of cool stuff apart from the ready-made templates. For instance, landing pages, plugins, email templates and many other products. 100k people are visiting the site every day. A team of 427 geeks is working for TemplateMonster. Their joint efforts let TemplateMonster reach $15M in revenue.

Template Monster landing page

Why should web professionals care about companies like TemplateMonster?

Because it’s beneficial for them. Cooperating with TemplateMonster frees their time, speeds their workflow, and lets them earn much more money.

Today all business owners understand how important it is to promote their services/products online. But not every entrepreneur is ready to pay for custom design, hire a developer, and create their online presentation from scratch.

They want to get their website within a reasonable budget, they want it to be quality, to look good, work flawlessly, and don’t wait for ages before their project will go live.

Web design agencies can cater to all these needs if they use our templates.

Tell me about the history of the company and how it was launched.

TemplateMonster was founded in 2002. Can you imagine that: people around the globe used our products when you knew nothing about Facebook and YouTube. We watched the evolution of the web and were proud to contribute into it. Hundreds of thousands websites you see today were built on the basis of our templates.

How everything started… I had been working at a custom design studio then. We tried our best to deliver top-quality products, but, unfortunately, most of our potential customers considered our services too expensive for them.

One day, I saw a designer who used a ready-made template to simplify and speed up his job. That was the moment when an idea to launch TemplateMonster crossed my mind. Eventually, this idea started to turn into a successful business.

Our company has held a leading position in the market for almost 15 years. Of course not all days were fine for us. We survived the rainy ones when something went totally wrong with our products. We got negative testimonials. And the bitter truth is that some of them were true. However, most of them referred to our outdated templates. What did we do? We just removed them from our marketplace.

They say “What doesn’t kill us makes us stronger”. And even negative feedback can be useful. We thoroughly analyzed it and took the right turn. We got deeper understanding of what our customers need, and gave it to them.

Example of a templateMonster template

Thanks for sharing your history. Why should one rely on a template?

Both entrepreneurs with little to no development skills and professional developers use our templates. All our customers get their benefits from our products.

The best thing about templates is that TemplateMonster’s customers see the final result, a ready-made product before paying any money for it.

Templates save time, money, efforts, nerves, whatever, and guarantee satisfaction with the future website.

What are the advantages of this approach instead of coding all by hand (or using Foundation or Bootstrap or other frameworks)?

Entrepreneurs get independence from designers, coders, and other professionals who sometimes overrate their work and don’t meet the deadlines. They don’t need to spend hours and hours searching for a responsible, skillful freelancers who may design something that they may be disappointed with in the end. As I have mentioned above, at TemplateMonster you see the final result at once. In other words, you see a ready-made product you are paying for. All you need to do is just replace the default content with your own.

Suppose you are just getting started and have no idea how to install the template, add your logo and other content, change colors, etc. You think only about the ways to generate more revenues and have no desire to mess with all those things.

So, you want to skip installation, customization or, say, integration with Google Analytics. Then, contact our Service Center. Our trained professionals will take care of everything and deliver you a ready-to-use site within 24 hours. Some tasks are completed even faster, i.e.: we install the template and plugins within 3 hours. There’s no issue members of our Service Center can’t cope with.

As to the coders knowing Bootstrap and other frameworks. Developers who are able to build sites themselves pay money for our products because it is advantageous for them.

With the help of our templates, developers considerably speed up their working process. They deliver more projects and earn more money respectively. We have a vast choice of templates in stock, which means that anyone can find the theme that meets the requirements even of the most picky customers. Creating something from scratch simply makes no sense if our marketplace offers so many ready-made designs. It’s like reinventing the wheel. Smart developers prefer to customize something here and there and deliver the website to the customer as quickly as possible.

I would like to tell you about the project we have launched not so long ago. It gives developers from all over the world an opportunity to get an official certificate from TemplateMonster that proves their skills. They just need to complete a course and then pass a final test (or pass the quiz at once) at our Certification Center. Having a certificate from a globally recognized web design company is a great way to improve your online image and look more credible for the customers.

Besides, having the certificate from TemplateMonster lets you get into Web Studios Catalogue, which gives a heap of additional opportunities.

You raise good points about certification. That is why Web Professionals has been certifying web designers, developers and more for so nearly 20 years.

What are the disadvantages of using a template?

Ok, you caught me;). It’s a tricky question, but I will answer it.

There is an opinion that using a template you fall under the risk to be unoriginal. If you’re going to use a template, then the chances are that you’re not alone, that’s the truth. But what is your chance of seeing a similar website on the Internet among millions of websites if you have bought your template from a marketplace like TemplateMonster with its terrific choice especially after customization? To my mind they are next to nothing. However, if it is crucial for you to be the one and only owner of the design, you can buy it out.

The quality of available templates varies, but in some cases, you might find the choices are rather basic. Some templates rely on you to fill in most of the gaps, and may have a poor set of graphics or visual elements. Frankly speaking I don’t see a big problem here as filling the template with your own content makes your website unique.

Using a template is unchallenging. Relying on templates to put together your projects, means you don’t get the benefits of learning the ins and outs of the software you’re using. But don’t you think that this is the essence of the template – to speed up and simplify the process of website launch? Not all entrepreneurs want to learn to code, design, and so on. They just want to get their benefit from the brand new website asap. A template gives them this possibility. In case they want to study everything that was left behind the scenes, they can sign up for one of our free educational projects (like “Your web studio in 61 days marathon“) and make up leeway.

61 days marathon

Templates are naturally designed to help you get the results as quickly and easily as possible, but in many cases the customization options can be limited, restricting what you can do with your files. That’s why it is important to read the template’s documentation carefully before the purchase. Don’t want to read? Watch the short video from our playlist on YouTube. Don’t want to watch the video or can’t find the relevant one, ask the support manager. They are always available to answer all your questions and help to choose the right template for your purchase. Don’t worry, the guys will work until you are 100% satisfied.

What other products/services do you offer?

2016 became the year when our team focused on developing flagship templates. They are much more multi-faceted compared to our regular products. Let me explain the things with using flagships on the example of WordPress templates. Of course I can’t help mentioning the recent release of Monstroid2 – Multipurpose WordPress Theme. It’s not an ordinary template, it’s a whole toolkit to build an online magazine, business site, personal portfolio, web store, whatever. You can create a complex portal combining several types of sites into one as well. Supposing you want the impossible from a single template: to present your company, plus share some interesting info with clients and sell products at the same time. It’s hard to believe, but using Monstroid2, you can build a business site, add blog and store functionalities to it. Monstroid2 is a one size fits all solution for all the needs you may have.

Here’s how it was. At first we created a flagship for WordPress because it’s the most popular CMS in the world. But then we decided to develop flagships for all popular engines: Joomla, OpenCart, PrestaShop, etc.

But that’s not all, we didn’t forget about the guys who don’t use any CMS at all. You can find new flagships among HTML5 templates. You already know that it’s one of our goals to meet the needs of everyone who comes to TemplateMonster marketplace.

Please note that we don’t charge more for flagship products. You can get any of them for the price of a regular template. Considering professional 24/7 support that we provide for a lifetime, our flagships are the best deals you can find on the market today.

It also should be mentioned that the users can figure out everything by themselves, without professional help. Every template comes well documented. The instructions guide the users through all ins and outs of using it. There are also numerous detailed tutorials at our Help Center and Startup Hub for those of you who are just at the start or want to learn how to handle their template by themselves. What’s more, we run a blog to share a lot of educational content with our audience, particularly, free eBooks, webinars, tips, tools to become more productive, and much more. At TemplateMonster, you won’t just learn how to build beautiful and functional websites lightning-fast, you’ll learn to enjoy the job.

What differentiates you from the competition?

The cost and value of our products comes to my mind first. Our prices are not higher than the ones of our competitors. If you want to save, just search Google and you will always find promo codes to reduce the price by 10%-40%. We always offer great discounts on Christmas, Independence Day, and other public holidays.

Sometimes you find out that the price for this or that template is a bit higher, but, remember about the value we provide. All our customers get more goodies as bonuses to their templates. For example, all our products, except for GPL WordPress themes, are delivered with HD images shown in the demo. It’s a good opportunity to save, as there’s no need to buy stock photos. At TemplateMonster, you can also benefit from free professional technical support.

How long are your templates supported?

TemplateMonster is the only website developer that provides this service for a lifetime without charging any extra payment now. Our competitors provide it for free only for a limited period.

I don’t think it’s fair. Some people don’t use the template straight away. It’s your right to decide when to use the product you paid for. But with a time limit on free support, you’ll have to pay extra money to get consultation, say, in half a year or stay on your own with your issues.

This is not our method. We are ready to help our customers any time at TemplateMonster (the same day, in a week, in a month, in a year, and so on). What’s even more important, our team of experts works until it’s over. Every customer should be absolutely satisfied.

Though, words are not a weighty argument. Thanks to our unsurpassed customer service, we entered the top three of web design companies per the TrustPilot rating. Do you believe this bullet-proof resource with verified customers reviews? So many people can’t be under a delusion.

Here is a video to prove my words.

David, what happens with a purchased template as web technologies continue to evolve?

It’s a good question. You need to update your site regularly and redesign it from time to time if you don’t want to look outdated. Trends are changeable, you’d better not miss the moment when your site starts looking rusty. Customers never take your seriously if your corporate web presentation looks outmoded.

I also recommend you to check how user-friendly your site is in terms of navigation, readability, and other essential aspects. It’s very important to test how it works on smartphones and tablets all of us use to browse the web on the go. Your site must adjust to all modern mobile devices, otherwise you will lose clients. If your site is not mobile-friendly, you can forget about high SEO rankings. Google doesn’t like such kind of sites.
BTW, flagships owners may not worry about the matter. Their websites will serve them for many years to come as regular updates are included into templates packages prices.

Thank you very much, David. You provided lots of thought provoking information for both practicing and aspiring web professionals. Have any more questions for David? Ask them in the comment section below.

The post Interview with David Braun from TemplateMonster appeared first on Web Professionals.

View full post on Web Professional Minute

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Happy New Year from

From all of us at, we hope that you had a wonderful holiday season. We are gearing up a busy and productive year, and want to take a moment to thank you for your ongoing support and commitment to our organization that has enabled us to progress on many fronts. We want to make even greater strides in 2016.

In the coming months, we will be focusing on resources that we hope will support your efforts. As a community driven organization we plan to reach out to a number of practicing Web professional experts. For example we’re working on the following articles for the New Year and we would like to invite you to be an active participant.

  • Latest Web Design, Web Development and Web Business Trends for 2016
  • Web Professional Opportunities including resources for those that freelance, small business, government and corporate Web professionals
  • Current Practices that work including Web security and marketing
  • Networking Opportunities
  • Ethics in our Web Profession including fostering a beneficial and respected professional and ethical approach to professional practice.
  • Sustainability including how we continue to advance the relevance, reputation and compensation of our profession. This includes having a steady stream of qualified new talent through education and training.

If you haven’t subscribed yet please do so. We think this is going to be a most exciting year for our organization and our profession. Although the pace of change continues, this seems to be the year when so many aspects of technology are converging.

What do you plan to accomplish in 2016?

Drop us a note and let us know what your plans are. If you have anything to share with the Web Professional community let us know that too!

Best always,
Mark DuBois, Community Evangelist and Director of Education

The post Happy New Year from appeared first on Web Professionals.

View full post on Web Professional Minute

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

From game jam to mobile and Firefox OS

I love participating in game jams, where game developers get together to craft games in a very short amount of time. I thought it would be cool to take one of my past Game Jam games, Metal vs Hipsters, and port it to the mobile web and Firefox OS.

Metal vs Hipsters (original)

Adapting to mobile

My starting point was an HTML5 game built for desktop. It had a fixed resolution, was controlled by the keyboard, etc. Some minor (and major!) tweaks would be required to make it a mobile game.

The game uses the open source game engine Phaser, so some of the code shown here will make use of Phaser’s API.

You can check out and play the final mobile version here.

Visualising the game

The first tool I used was Firefox’s Responsive Design View. It allows you to crop the screen and simulate different screen sizes. You can access it from the Firefox browser Tools drop-down menu under Tools>Web Developer>Responsive Design View.

The other tool was an actual mobile phone! I have an Android device with several browsers installed. By running the game in localhost and then connecting my phone to my local Wi-Fi network, I could access the game at my computer’s IP.

And this would yield the first problems…


This is how bad the game looked in the responsive design view with no scaling at 320×480 size (landscape):

Scaling issues

The solution was to use Phaser’s ScaleManager to scale the game so it would fit well on the screen (with letter-boxing if needed):

var BootScene = {
  init: function () {
    // make the game occuppy all available space, but respecting
    // aspect ratio – with letterboxing if needed = Phaser.ScaleManager.SHOW_ALL; = true; = true;
  // ...

Furthermore, in the original game I used a wrapping div element to center the game on the page and also add some padding, text, etc. Now, all these page elements would have to be removed for the game to access all the available screen space.

Proper scale

Much better now!


The next issue was controls. My desktop game used the arrow keys to move and jump the main character. I opted to include a virtual gamepad, so I created a new image for it:

Virtual gamepad

I needed three buttons: two for moving left and right and another one for jumping. I created three Phaser.Button instances. This was done in a helper function:

var upBtn = new Phaser.Button(, ...);

Since Phaser.Button doesn’t have a flag that tells us whether a button is pressed or not, I needed to add it myself. This is easily done by listening to inputDown and inputUp events.

[leftBtn, rightBtn, upBtn].forEach(function (btn) {
  // register callbacks
  btn.isDown = false; () { btn.isDown = true; }); () { btn.isDown = false; });

We also need to listen to the Up button’s inputDown, to enable the character to jump, since jumping in the desktop game was triggered by a keyDown event:, this);

For moving, it was just a matter of checking the isDown flag I’d already set, in addition to the previous check against the this.keys.left keyboard key.

if (this.keys.left.isDown || this.keys.btnLeft.isDown) {

Added virtual gamepad

The game was then playable on my device in the Firefox and Chrome for Android mobile browsers. However, play testing revealed even more issues…

Reposition elements

While I was playing the game I noticed that my own fingers would hide the game characters. This was a huge deal, since it made the game very difficult and annoying to play.

The solution was to make the group bigger (100 pixels) and then shift all the game elements up.

Reposition ground and characters

Allow tapping on additional areas

I also found out that tapping on the click to restart text was a bit hard. So I made the Game Over text, which was bigger, clickable too.

// create game over text
var banner =;
banner.inputEnabled = true;, this);

In addition to that, I substituted click to restart for tap to restart, which makes more sense in mobile environments.

Revamping the splash screen

The splash screen needed some love too, since it was originally hardcoded to a fixed resolution.

The first step was to resize the background to take up the whole screen space, while maintaining its aspect ratio:

.overlay {
  background-size: cover;

Then I resized some of the existing text and buttons, and deleted the controls help diagram, since it referred to desktop arrow keys.

Splash screen


Finally, the game was fully playable… but now it was too short! The original game had five predefined waves of enemies. It was fine for the game jam, since most people only try the games for a very short amount of time (there are literally thousands of games to try out).

For a mobile game, one quick minute of gameplay is not good enough. So I tweaked the gameplay code to get rid of the predefined waves and make them random and infinite. Unlimited playtime!

Transform into a Firefox OS app

Once the game worked decently and was fun to play, it was time to turn it into a Firefox OS app.

Run in the simulator as a hosted app

The first step was to run it as a hosted app in a Firefox OS Simulator.

You can get the Simulator from the WebIDE (Tools > Web Developer > WebIDE) by clicking Install Simulator in the right sidebar. I installed Firefox OS version 2.6.

Install simulator

To create a hosted web app, we need an icon referenced in a JSON file with some metadata. This file is called the web app manifest.

While you can provide different sized icons, the minimum requirement is to have a 128x128 one:

Icon 128x128

The manifest file is just a JSON file in our app’s root. It contains some metadata about our app: the name of the app, the author, locales, etc. This is what the initial version of my manifest.webapp looked like.

  "name": "Metal vs Hipsters",
  "description": "Arcade game",
  "launch_path": "/index.html",
  "icons": {
    "128": "/images/icon-128.png"
  "developer": {
    "name": "Belén 'Benko' Albeza",
    "url": ""
  "default_locale": "en",
  "version": "1.0.0"

To run Metal vs Hipsters in the Simulator from the WebIDE, click on Open Hosted App and then point to the location of the manifest in the running local server:

Open Hosted App

Manifest URL

Now my project has been added…

Hosted web app

…and we can finally run it in the simulator.

Game running on simulator

Run in the phone as a packaged app

Running a game in the simulator is cool, but nothing beats playing your game on a real device. I wanted to create a packaged app in addition to a hosted app for Firefox Marketplace.

I had an Xperia Z3 Compact with Firefox OS on it ready for action. Once plugged in via USB and with developer mode enabled, it was listed in the WebIDE on the USB devices list.

Devices list

The workflow for creating a packaged app is very similar to the one I described for a hosted app. In the WebIDE click on Open Packaged App and then point to the game’s folder:

Packaged app

With my phone selected as a device, I clicked on the “play” button in the top bar and… boom!

Game running on Firefox OS phone

Now that I had the game running on a phone I could take advantage of some settings available only for apps and not just mobile websites. I wanted the game to run in fullscreen (without any prompting!) and I wanted to force a landscape orientation. I was able to do this by adding some more settings to the manifest.

  "fullscreen": "true",
  "orientation": ["landscape"]

Game running in fullscreen

Local assets

Now that I’d built the game as a packaged app, I needed to include all the assets needed to run it. The game uses one font from Google Fonts, Bangers. For the game jam, I included the font using one of Google’s suggested methods: linking to CSS on their servers that contains the font definition.

<link href=''
 rel='stylesheet' type='text/css'>

As you can see, this won’t work for a packaged app, since it’s possible for a player to launch the game for the first time while offline, and they would not be able to access the font. Luckily, these fonts are also available for download and distribution with your projects (yay for open source!), so I did just that.

I copied the content of Google’s CSS file to my own CSS, changed the src URL to point to my local copy, and everything was set!

@font-face {
  font-family: 'Bangers';
  font-style: normal;
  font-weight: 400;
  src: url(../fonts/Bangers.woff2) format('woff2');

Into the Firefox Marketplace

Packaging for Firefox OS

A packaged app is distributed via a single zip file with our application on it. I was using a task runner, Gulp, to automate some parts of the development; so I added a new task to generate this zip file automatically. I made use of the gulp-zip module.

npm install gulp-zip --save
gulp.task('release', ['dist'], function () {
  var data = JSON.parse(fs.readFileSync('./package.json'));
  var filename = + '-' + data.version + '.zip';

  return gulp.src('./dist/**/*')

Tip: once generated, always unzip your file to make sure everything is there!

Submitting your app to Firefox Marketplace

You can submit your app to the Marketplace, but you will need to create an account first.

Once logged in, I selected Free App, then Packaged and uploaded the zip file. Then the manifest and app files were validated.

App validation

There were some warnings related to the CSP, but since the game doesn’t ask for special permissions (like access to the contacts), it’ll be OK. These CSP warnings were due to Phaser using document.createElement('script') when using bitmap fonts. Since my game wasn’t using these fonts, I could just monkey patch Phaser and remove that code bit if I really needed special permissions for my game.

In order to submit the game, I had to provide a privacy policy. The game wasn’t collecting or storing any sort of data, so I settled by writing just that:

This application does not store or collect personal information or any kind of user data.

If your game does collect data, check out the privacy policy guidelines in the MDN to help you draft one.

I also needed some 320x480 screenshots:

Marketplace screenshot

As the last step, I was asked to setup content rating. The submission form takes you to the IARC tool, which is a wizard to help you rate your game depending on some criteria. Since my game features a form of fantasy violence –I guess a guitar of flames that destroys hipsters and cupcakes counts as so–, this ended up rated as PEGI-12.

PEGI rating

All done! Now time to wait a bit until it gets reviewed and approved.

More tweaks and potential improvements

Not all browsers have the same support of features or file formats. While testing the game in different devices, I found out some issues with Safari on iOS: the webfont wasn’t loaded properly and the background music tracks was not being played. The issue was that Safari for iOS doesn’t support neither Woff nor OGG as formats for fonts and audio (tip: you can check these things in Can I use?). All I had to add to be able to play the game in my iPad was to add those assets in TTF and MP4 as well.

The game should be playable in major major mobile browsers (Firefox, Chrome and Safari). I expect trouble with IE/Edge, but I don’t have a Windows device at hand to test this.

Although the current version is fun to play in browsers, there are other improvements that could make this a better game: use of the vibration API when the main character gets killed, store the high-score with localStorage, allow installation of the app directly from the game’s website, support for offline mode in the web version, etc.


The game is already available in the Firefox Marketplace for downloading. You can also play the game from your mobile browser here, and grab the full source code at Github as well.

I hope this article can help to port your HTML 5 games to mobile and Firefox OS. Most of the times they don’t need much tweaking and you could expand your player base. Plus playing your own game while commuting is priceless 🙂

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Web Push notifications from Irssi

Our main communication tool at Mozilla is IRC. I’m running an IRC client called Irssi under screen on a server constantly connected to the network. It’s a close-to-perfect solution with only two outstanding issues for me. One is the lack of emoji characters (I can live with that). The other is more important: there is no easy way to install a solution to receive notifications. This gave us the idea for a small project — to build a generic server for sending notifications that could work alongside the Irssi client.

Overview of the application

Mercurius is a generic push server that lets developers bring Web Push into their own native/web applications. Subscription management is entirely handled by Mercurius; the user registers on the Mercurius website and receives a token that other applications can use to send customized push notifications via a REST API.
Mercurius uses the web-push Node.js library to handle communication with the push service and takes care of the necessary payload encryption.
You can find the code on GitHub.


There are four actors in our application:

  • The user agent (browser), which subscribes for push notifications on a push service and sends the subscription information to the application server (Mercurius);
  • The push service;
  • The Mercurius application server, which maintains a list of the user subscriptions and sends push notifications to users via the push service;
  • A Mercurius client (in this case it’s an Irssi plugin), which uses a token (given to the user by the Mercurius server) to send the user push notifications via the Mercurius server.

Architecture diagram

Subscribing to push notifications

We needed to build a web page to allow users to subscribe for push notifications. The PushManager interface of the Push API comes to our aid. It can be accessed through a service worker registration:

.then(function(registration) {
  // registration.pushManager

It provides a function to subscribe the user and a function to get an already existing subscription. We’ll try to get an existing subscription first and, if that fails, we’ll re-register the user:

return registration.pushManager.getSubscription()
.then(function(subscription) {
  if (subscription) {
    return subscription;

  return registration.pushManager.subscribe({ userVisibleOnly: true })
  .then(function(newSubscription) {
    return newSubscription;

Once we have the subscription (comprised of an endpoint URL on the push service and a user’s public key), we can send its info to the server, which will then use it to send the user a notification.
The Fetch API sends the POST request to the server (just to spice up the demo a bit):

var key = subscription.getKey ? subscription.getKey('p256dh') : '';

fetch('./register', {
  method: 'post',
  headers: {
    'Content-type': 'application/json'
  body: JSON.stringify({
    endpoint: subscription.endpoint,
    key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '',

Keeping the subscription up to date

The user’s subscription to the push service is periodically refreshed, or, in some cases invalidated (see the Push Quota recipe in the ServiceWorker Cookbook for an in-depth look), so we need to always keep the server up to date with the user subscription information.
To do so, we’ll add an event listener in the service worker for the ‘pushsubscriptionchange‘ event:

self.addEventListener('pushsubscriptionchange', function(event) {
  // Handle the event by re-subscribing the user and sending the new info to the server

This way, when the subscription changes, we notify the server of the new endpoint URL and the new key.

Showing visual notifications

Once our user is subscribed to the push service and to the Mercurial server, we can send push messages and show visual notifications. The Irssi plugin will send a POST request to the Mercurius server, asking it to send the notification to the user. The Mercurius server sends the notification to the push service, which then delivers the notification to the browser. The payload of the push notification is sent to the endpoint URL with some clever crypto. It provides the actual parameters for displaying the notification. (You can read more here about the encryption:
The push notification triggers a ‘push‘ event in the service worker registered for the page, re-launching the service worker if it it is no longer active.
So, to show something to the user, we just need to handle the ‘push‘ event in our service worker:

self.addEventListener('push', function(event) {
  var data = ? : null;

  var title = data ? data.title : 'Mercurius';
  var body = data ? data.body : 'Notification';

  event.waitUntil(self.registration.showNotification(title, {
    body: body,

As we mentioned earlier, we’re using the payload of the push message (for reference, see the Push Payload recipe in Mozilla’s Service Worker Cookbook) to pass the parameters of the notification from the Irssi plugin (or any other Mercurius client) to the service worker.

Testing with Mocha

We have implemented a few BDD tests with the Mocha framework to verify that the Mercurius server works correctly. We’ve also employed istanbul to measure code coverage and guide our test writing activity.
Simply run npm test to see its output.


Deploying the server to Heroku is really simple. We have chosen to make Travis CI, our continuous integration service, perform the deploy after building Mercurius and a successful test run. Here’s our self-explanatory YAML configuration file for Travis CI:

language: node_js
  - '0.12'
  - '4'
  provider: heroku
    app: mozcurius
    skip_cleanup: true
      repo: marco-c/mercurius
      branch: master
      node: '4'

We use skip_cleanup because we want to publish our build artifacts to Heroku.
The (encrypted) API key is specific to your user account and can be created using the Heroku and Travis CI command line clients:

travis encrypt $(heroku auth:token) --add deploy.api_key

For more information about integrating Heroku deployment in Travis CI, see

Irssi client

The role of the Irssi Mercurius client is to send a notification to the Mercurius server every time the user is mentioned. I've modified an existing script, which saves this info to a file, so that it now saves and sends the request to the Mercurius server. After loading the script, a new command is registered in Irssi. It's called /mercurius, and it sets the token, host (if you decide to run your own), and intensity of the notifications. There is also a method to stop and start again notifications at will.

The plugin is written in Perl. Sending requests to the Mercurius server happens in the sub notify using the built-in HTTP::Tiny module. I've used the request method instead of post for backwards compatibility with older Perl versions.
The subroutine responsible for sending the notification request is called notify. Description is given here inside the comments.

use HTTP::Tiny;
use JSON::PP;
# [...]
sub notify {
	if ($enabled) {
		my ($text) = @_;
		# $host is a global variable set with "set_host" command
		my $url = $host . '/notify';
		my $http = HTTP::Tiny->new();
		my $data = encode_json {
			# $token is a global variable set with "set_token" command
			"token" => $token,
		    	"payload" => {
				"title" => "IRSSI", 
				"body" => $text
		my $response = $http->request('POST', $url, {
				content => $data,
				headers => {"Content-Type" => "application/json"}
		# [...]

The plugin is placed on GitHub along with install instructions.


/mercurius set_token {TOKEN}
Use it to set the token. You will receive a notification confirming that it has been set.

/mercurius set_host {HOST}
Default value is "". Please note that the trailing slash has been removed.

/mercurius set_intense {0/1}
Switches on/off intense mode (default 0). With intense mode (1) all of the notifications are sent. Otherwise (default) if the user is in an active private window only the first message from that window will send a notification.

/mercurius stop and /mercurius start are self explanatory.

View full post on Mozilla Hacks - the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Quick tip: stop Powerpoint from breaking words into a new line

With my talk decks needing more re-use in the Windows/Microsoft community, I am trying to use Powerpoint more and wean myself off the beauty of Keynote (and its random crashes – yes, all software sucks).

One thing I realised today is that Powerpoint thinks it is sensible to break words anywhere to go to a new line, not by word, or even syllable, but by character:

default line break setting
Words are broken into new lines at any character, which makes alignment a not enjoyable game of “find the breakpoint”

This is the preset! To get rid of it, you don’t need to summon the dark lord, but all you need to do is to unset the default. You can find this in:

Format ? Paragraph ? Line Breaks and Alignment ? uncheck: “Allow Latin text to wrap in the middle of a word”

Here’s a recording to show the difference:

fixed line break setting
By unsetting the preset you can do what you want – line breaks are now only possible after full words

Why this would be a preset is beyond me. Now I can breathe freely again.

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Creating a mobile app from a simple HTML site: Part 4

How to polish your app and prepare it for market

In previous sections of this step-by-step series (Part 1, Part 2, and Part 3) we’ve created an app that loads multiple school plans from the server.

What we have so far is functional, but still has a number of issues, including two which are major: no offline mode and a hard-coded configuration. In this closing post, we’ll work on all of these issues.

If you haven’t built up the examples from the previous parts, use stage7 app and stage7 server as a starting point. To get started, follow the instructions on how to load any stage in this tutorial.


First, let’s do some refactoring. We’ll add a User object type to make it easier to choose which user’s plan to display. This will allow us to remove some more code from the app’s main logic flow, making the code cleaner and more modular. This object will load data from the server to create the plan UI. At the moment, rendering is done in app.onDeviceReady and app.renderData methods. Our new onDeviceReady method creates an app.user object and runs the methods needed to get and render plans. Replace the existing onDeviceReady method with the following:

onDeviceReady: function() {
    app.user = new User('johndoe');


Next, delete the app.renderData method completely. We will store this functionality inside our new User object instead, as you’ll see below.

Next, at the beginning of the index.js file (before the definition of Plan), add the following code to define the User object:

function User(userid) { = userid;
    this.plans = [];

User.prototype.getPlans = function() {
    var self = this;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        var plans = JSON.parse(this.responseText);
        for (var i = 0; i < plans.length; i++) {
            self.plans.push(new Plan(plans[i]));
    };"get", app.getPlansURL +, true);

User.prototype.displayPlans = function() {
    var self = this;
        var deck = document.getElementById('plan-group');
        var tabbar = document.getElementById('plan-group-menu');
        for (var i = 0; i < self.plans.length; i++) {
            self.plans[i].createUI(deck, tabbar, dayOfWeek);
    }, function() {}, {type: 'narrow', item: 'days'});

This contains functionality previously found in renderData and onDeviceReady. After the plan’s JSON is loaded from the server we iterate over the list and create a list of Plan objects in user.plans. We then call user.displayPlans, which creates the required DOM environment and invokes plan.createUI for each element of user.plans.

You may notice that the above code uses app.getPlansURL to allow the XHR call to find the JSON. Previously the URL was hardcoded into"get", "", true);. Since it’s better to keep settings in one place we’ll add this as a parameter of the app object instead.

Add the following into your code:

var app = {
    getPlansURL: "",
    // ....

Now try preparing your app again with the cordova prepare terminal command, starting your server, and reloading the app in WebIDE (as we’ve done in previous articles). The app should work as before.

Offline mode

Let’s turn our attention to making the app work offline.

There are several technologies available to store data on the device. I’ve decided to use the localStorage API, as the data we need to store is basically just simple strings and doesn’t need any complex structuring. As this is a key/value store, objects need to be stringified (represented as a string). I will use only two functions — setItem() and getItem(). For example, storing a user id would require you to call localStorage.setItem('user_id',;.

There are two values we definitely need to store in the phone memory — the ids of the current user and the plans. To make the app fully open and allow users to use servers not provided by the app creators we also need to store the server address. We’ll start by storing the plans. The user id and server will be still hardcoded. Let’s add a new method — User.loadPlans — and call that from app.onDeviceReady, instead of calling both user.getPlans() and app.user.displayPlans. We will decide if the plans need to be loaded from the server and then display the plans by calling User.displayPlans() from User.loadPlans.

First update onDeviceReady again, to this:

onDeviceReady: function() {
    app.user = new User('johndoe');

Now add in the following definition for the loadPlans method, up near your other User object-defining code:

User.prototype.loadPlans = function() {
    var plans = localStorage.getItem('plans');
    if (plans === null) {
        return this.getPlans();
    console.log('DEBUG: plans loaded from device');

The above method calls another new method — initiatePlans. This parses the JSON representation of the plans and initiates Plan objects inside the User.plans array. In the future we would like to be able to reload the plans from the server, but the existing code always adds new plans instead of replacing them.

Add the following definition just below where you added the loadPlans code:

User.prototype.initiatePlans = function(plansString) {
    var plans = JSON.parse(plansString);
    for (var i = 0; i < plans.length; i++) {
        this.plans.push(new Plan(plans[i]));

We also need to store the plans on the client. The best point to do this is right after the plans have been loaded from the server. We will call localStorage.setItem('plans', this.responseText) inside the success response function of getPlans. We must also remember to call the initiatePlans method.

Update your getPlans definition to the following:

User.prototype.getPlans = function() {
    var self = this;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        console.log('DEBUG: plans loaded from server');
        localStorage.setItem('plans', this.responseText);
    request.onerror = function(error) {
        console.log('DEBUG: Failed to get plans from ``' + app.getPlansURL + '``', error);
    };"get", app.getPlansURL +, true);

At this moment, successful loading of the user’s plans happens only once. Since school plans change occasionally (usually twice a year) you should be able to reload the plans from the server any time you want. We can use User.getPlans to return updated plans, but first we need to remove existing plans from the UI, otherwise multiple copies will be displayed.

Let’s create User.reloadPlans — add the code below your existing User object-defining code:

User.prototype.reloadPlans = function() {
    // remove UI from plans
    for (var i = 0; i < this.plans.length; i++) {
    // clean this.plans
    this.plans = [];
    // clean device storage
    localStorage.setItem('plans', '[]');
    // load plans from server

There is a new method to add to the Plan object too — Plan.removeUI — which simply calls Element.remove() on a plan’s card and tab. Add this below your previous Plan object-defining code:

Plan.prototype.removeUI = function() {

This is a good time to test if the code has really reloaded. Run cordova prepare again on your code, and reload it in WebIDE.

We’ve got two console.log statements inside loadPlans and getPlans to let us know if the plans are loaded from device’s storage or the server. To initiate a reload run this code in Console:


Note: Brick reports an error at this point, as the link between each tab and card has been lost inside the internal system. Please ignore this error. Once again, our app still works.

reloadPlans from Console

Let’s make the reload functional by adding a UI element to reload the plans. In index.html, wrap a div#topbar around the brick-tabbar. First, add a reload button:

<div id="reload-button"></div>
<div id="topbar">
    <brick-tabbar id="plan-group-menu">

Now we’ll make the button float to the left and use calc to set the size of the div#topbar. This makes Brick’s tabbar calculate the size when layout is changed (portrait or horizontal). In css/index.css, add the following at the bottom of the file:

#topbar {
	width: calc(100% - 45px);

#reload-button {
	float: left;
	/* height of the tabbar */
	width: 45px;
	height: 45px;
	background-image: url('../img/reload.svg');
	background-size: 25px 25px;
	background-repeat: no-repeat;
	background-position: center center;

We will download some Gaia icons to use within the application. At this stage, you should save the reload.svg file inside your img folder.

Last detail: Listen to the touchstart event on the #reload-button and call app.user.reloadPlans. Let’s add an app.assignButtons method:

assignButtons: function(){
    var reloadButton = document.getElementById('reload-button');
    reloadButton.addEventListener('touchstart', function() {
    }, false);

We need to call it from app.deviceReady before or after app.activateFingerSwipe().

onDeviceReady: function() {
    // ...

At this point, save your code, prepare your cordova app, and reload it in WebIDE. You should see something like this:

reload button

Here you can find current code for app and server.

Settings page

Currently all of the identity information (the address of the server and user id) is hardcoded in the app. If the app is to be used with others, we provide functionality for users to set this data without editing the code. We will implement a settings page, using Brick’s flipbox component to change from content to settings and vice versa.

First we must tell the app to load the component. Let’s change index.html to load the flipbox widget by adding the following line into the HTML <head> section:

<link rel="import" href="app/bower_components/brick-flipbox/dist/brick-flipbox.html">

Some changes must happen inside the HTML <body> as well. brick-topbar and brick-deck need to be placed inside switchable sections of the same flipbox, and the reload button also moves to inside the settings. Replace everything you’ve got in your <body> so far with the following (although you need to leave the <script> elements in place at the bottom):

    <section id="plans">
        <div id="settings-button"></div>
        <div id="topbar">
            <brick-tabbar id="plan-group-menu">
        <brick-deck id="plan-group">
    <section id="settings">
        <div id="settings-off-button"></div>

The idea is to switch to the “settings” view on pressing a settings-button, and then back to the “plans” view with the settings-off-button.

Here’s how to wire up this new functionality. First up, inside your app definition code add a toggleSettings() function, just before the definition of assignButtons():

toggleSettings: function() {

Update assignButtons() itself to the following (we are leaving the reloadButton code here, as we’ll need it again soon enough):

assignButtons: function() {
    app.flipbox = document.querySelector('brick-flipbox');
    var settingsButton = document.getElementById('settings-button');
    var settingsOffButton = document.getElementById('settings-off-button');
    settingsButton.addEventListener('click', app.toggleSettings);
    settingsOffButton.addEventListener('click', app.toggleSettings);
    var reloadButton = document.getElementById('reload-button');
    reloadButton.addEventListener('touchstart', function() {
    }, false);

Next: we need to retrieve some more icons for the app. Save the settings.svg and back.svg files inside your img folder.

Let’s update our css/index.css file too, to add the additional icons in and style our new features. Add the following at the bottom of that file:

#form-settings {
    font-size: 1.4rem;
    padding: 1rem;
#settings-button, #settings-off-button {
    float: left;
    width: 45px;
    height: 45px;
    background-size: 25px 25px;
    background-repeat: no-repeat;
    background-position: center center;
#settings-button {
    background-image: url('../img/settings.svg');
#settings-off-button {
    background-image: url('../img/back.svg');
    border-right: 1px solid #ccc;
    margin-right: 15px;
brick-flipbox {
    width: 100%;
    height: 100%;

If you now prepare your cordova app again with cordova prepare, and reload the app inside WebIDE, you should see something like this:

flipbox working

Adding a settings form

To add a form and some data, replace your current <section id="settings"></section> element with the following:

<section id="settings">
    <div id="settings-off-button"></div>
    <form id="form-settings">
        <p><label for="input-server">Server</label></p>
        <p><input type="text" id="input-server" placeholder=""/></p>
        <p><label for="input-user" id="label-user">User ID</label></p>
        <p><input type="text" id="input-user" placeholder="johndoe"/></p>
        <p><button id="reload-button">RELOAD ALL</button></p>

Making it look good is easy! We’ll use some ready-made CSS available in Firefox OS Building Blocks. Download input_areas.css and buttons.css and save them in your www/css directory.

Then add the following to your HTML <head>, to apply the new CSS to your markup.

<link rel="stylesheet" type="text/css" href="css/input_areas.css">
<link rel="stylesheet" type="text/css" href="css/buttons.css">

Go into the css/index.css file and delete the #reload-button { ... } rule to make the “RELOAD ALL” button display correctly.

Run cordova prepare again and reload the app in WebIDE, and you should see something like this:


We’ve got the view — now to support it. When the app first installs there will be no setting, so the app will have no idea how to load the data. Instead of showing empty plans we immediately toggle the view to the settings. Update your onDeviceReady function as shown:

onDeviceReady: function() {
    app.plansServer = localStorage.getItem('plansServer');
    app.userID = localStorage.getItem('userID');

    if (app.plansServer && app.userID) {
        app.user = new User(app.userID);
    } else {

The app needs to read the data from our form inputs, so save them into the phone’s storage, and reload after the change. Let’s add this functionality at the end of the app.assignButtons function. First we will stop the form from submitting (otherwise our app would just reload the index.html instead of redrawing the content). Add the following code to the end of the assignButtons function, just before the closing },:

document.getElementById('form-settings').addEventListener('submit', function(e) {
}, false)

We will read and set the input value for the server. I’ve decided to read the input value on the blur event. It is dispatched when the user touches any other DOM Element on the page. When this happens, the server value is stored in the device’s storage. If app.plansServer exists on app launch, we set the default input value. The same has to happen for the userID, but there is a special step. Either we change the app.user or create a new one if none exists. Add the following just above the previous block of code you added:

var serverInput = document.getElementById('input-server');
serverInput.addEventListener('blur', function() {
    app.plansServer = serverInput.value || null;
    localStorage.setItem('plansServer', app.plansServer);
if (app.plansServer) {
    serverInput.value = app.plansServer;    

var userInput = document.getElementById('input-user');
userInput.addEventListener('blur', function() {
    app.userID = userInput.value || null;
    if (app.userID) {
        if (app.user) {
   = app.userID;
        } else {
            app.user = new User('app.userID');
        localStorage.setItem('userID', app.userID);
if (app.userID) {
    userInput.value = app.userID;    

After preparing your app and reloading it in WebIDE, you should now see the settings working as expected. The settings screen appears automatically if no plans are found in the localstorage, allowing you to enter your server and userID details.

Test it with the following settings:

  • Server:
  • User ID: johndoe

The only inconvenience is that you need to switch back to the plans view manually once Settings are loaded. In addition, you might get an error if you don’t blur out of both form fields.

settings are working

Improving the settings UX

In fact, switching back to the plans view should be done automatically after plans are successfully loaded. We can implement this by adding a callback to the User.getPlans function, and including it in the call made from User.reloadPlans:

First, update your getPlans() function definition to the following:

User.prototype.getPlans = function(callback) {
    var self = this;
    var url = app.plansServer + '/plans/' +;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        console.log('DEBUG: plans loaded from server');
        localStorage.setItem('plans', this.responseText);
        if (callback) {
    };"get", url, true);

Next, in User.reloadPlans add the callback function as an argument of the this.getPlans call:


Again, try saving, preparing, and reloading your new code. You should now see that the app displays the plans view automatically when correct server and user information is entered and available. (It switches to the Settings view only if there is no error in self.initiatePlans(this.responseText).).


Where do we go from here

Congratulations! You’ve reached the end of this 4-part tutorial, and you should now have your own working prototype of a fun school plan application.

There is still plenty to do to improve the app. Here are some ideas to explore:

  • Writing JSON files is not a comfortable task. It would be better to add new functionality that lets you edit plans on the server. Assigning them to the user on the server opens new possibilities, and this would just require an action to sync the user account on both server and client.
  • Displaying hours would also be nice.
  • It would be cool to implement a reminder alarm for some school activities.

Try implementing these yourself, or build out other improvements for your own school scheduling app. Let us know how it goes.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)