├── img ├── push2.gif ├── realtime-logo.jpg ├── chrome_webpush.png ├── firefox_webpush.png └── realtime-large-logo.jpg ├── manifest.json ├── LICENSE ├── WebPushManager.js ├── index.js ├── README.md ├── service-worker.js ├── index.html └── css ├── normalize.css └── skeleton.css /img/push2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realtime-framework/WebPushNotifications/HEAD/img/push2.gif -------------------------------------------------------------------------------- /img/realtime-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realtime-framework/WebPushNotifications/HEAD/img/realtime-logo.jpg -------------------------------------------------------------------------------- /img/chrome_webpush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realtime-framework/WebPushNotifications/HEAD/img/chrome_webpush.png -------------------------------------------------------------------------------- /img/firefox_webpush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realtime-framework/WebPushNotifications/HEAD/img/firefox_webpush.png -------------------------------------------------------------------------------- /img/realtime-large-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realtime-framework/WebPushNotifications/HEAD/img/realtime-large-logo.jpg -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Realtime Web Push Demo", 3 | "short_name": "Web Push Demo", 4 | "gcm_sender_id": "103953800507" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Realtime Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /WebPushManager.js: -------------------------------------------------------------------------------- 1 | 2 | var WebPushManager = function(){ 3 | } 4 | 5 | WebPushManager.prototype.start = function(callback) { 6 | if ('serviceWorker' in navigator) { 7 | navigator.serviceWorker.register('./service-worker.js') 8 | .then(this.getRegistrationId(callback)); 9 | } else { 10 | callback('Service workers aren\'t supported in this browser.', null); 11 | } 12 | } 13 | 14 | WebPushManager.prototype.getRegistrationId = function (callback) { 15 | navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { 16 | 17 | var fb_messaging = firebase.messaging(); 18 | fb_messaging.useServiceWorker(serviceWorkerRegistration); 19 | 20 | fb_messaging.requestPermission() 21 | .then(function() { 22 | console.log('Notification permission granted.'); 23 | 24 | fb_messaging.getToken() 25 | .then(function(currentToken) { 26 | if (currentToken) { 27 | callback(null, currentToken); 28 | } 29 | }) 30 | .catch(function(err) { 31 | callback(err) 32 | }); 33 | }) 34 | .catch(function(err) { 35 | console.log('Unable to get permission to notify. ', err); 36 | callback(err); 37 | }); 38 | }); 39 | } 40 | 41 | WebPushManager.prototype.forceNotification = function(message) { 42 | navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { 43 | serviceWorkerRegistration.active.postMessage(message); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Generate the user private channel 2 | var channel = generateUserChannel(); 3 | 4 | $(document).ready(function() { 5 | 6 | // In this example we are using a demo Realtime application key without any security 7 | // so you should replace it with your own appkey and follow the guidelines 8 | // to configure it 9 | var RealtimeAppKey = "K4xqxB"; 10 | 11 | // update the UI 12 | $('#curl').text('curl "http://ortc-developers-useast1-s0001.realtime.co/send" --data "AK=' + RealtimeAppKey + '&AT=SomeToken&C=' + channel + '&M=12345678_1-1_This is a web push notification sent using the Realtime REST API"'); 13 | $('#channel').text(channel); 14 | 15 | // start Web Push Manager to obtain device id and register it with Realtime 16 | // a service worker will be launched in background to receive the incoming push notifications 17 | var webPushManager = new WebPushManager(); 18 | 19 | webPushManager.start(function(error, registrationId){ 20 | if (error) { 21 | 22 | if(error.message) { 23 | alert(error.message); 24 | } else { 25 | alert("Ooops! It seems this browser doesn't support Web Push Notifications :("); 26 | } 27 | 28 | $("#curl").html("Oops! Something went wrong. It seems your browser does not support Web Push Notifications.

Error:
" + error.message); 29 | $("#sendButton").text("No can do ... this browser doesn't support web push notifications"); 30 | $("#sendButton").css("background-color","red"); 31 | }; 32 | 33 | // Create Realtime Messaging client 34 | client = RealtimeMessaging.createClient(); 35 | client.setClusterUrl('https://ortc-developers.realtime.co/server/ssl/2.1/'); 36 | 37 | client.onConnected = function (theClient) { 38 | // client is connected 39 | 40 | // subscribe users to their private channels 41 | theClient.subscribeWithNotifications(channel, true, registrationId, 42 | function (theClient, channel, msg) { 43 | // while you are browsing this page you'll be connected to Realtime 44 | // and receive messages directly in this callback 45 | console.log("Received a message from the Realtime server:", msg); 46 | 47 | // Since the service worker will only show a notification if the user 48 | // is not browsing your website you can force a push notification to be displayed. 49 | // For most use cases it would be better to change the website UI by showing a badge 50 | // or any other form of showing the user something changed instead 51 | // of showing a pop-up notification. 52 | // Also consider thar if the user has severals tabs opened it will see a notification for 53 | // each one ... 54 | webPushManager.forceNotification(msg); 55 | }); 56 | }; 57 | 58 | // Establish the connection 59 | client.connect(RealtimeAppKey, 'JustAnyRandomToken'); 60 | }); 61 | }); 62 | 63 | // generate a GUID 64 | function S4() { 65 | return (((1+Math.random())*0x10000)|0).toString(16).substring(1); 66 | } 67 | 68 | // generate the user private channel and save it at the local storage 69 | // so we always use the same channel for each user 70 | function generateUserChannel(){ 71 | userChannel = localStorage.getItem("channel"); 72 | if (userChannel == null || userChannel == "null"){ 73 | guid = (S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); 74 | userChannel = 'channel-' + guid; 75 | localStorage.setItem("channel", userChannel); 76 | } 77 | return userChannel; 78 | } 79 | 80 | // send a message to the user private channel to trigger a push notification 81 | function send(){ 82 | if (client) { 83 | client.send(channel, "This is a web push notification sent using the Realtime JavaScript SDK"); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Push Notifications with Realtime 2 | This project shows how to use the Web Push Notifications API in a website, allowing easy engagement with users that are currently not browsing the website. This project uses the Realtime Messaging JavaScript SDK and requires Chrome 50+ or Firefox 44+. 3 | 4 | ## Realtime + Web Push Notifications guide 5 | 6 | - Register to get your free Realtime Messaging application key at [https://accounts.realtime.co/signup/](https://accounts.realtime.co/signup/) 7 | 8 | - Create a Firebase Cloud Messaging project. [Follow this tutorial](http://messaging-public.realtime.co/documentation/starting-guide/mobilePushGCM.html). 9 | 10 | - Open the `index.html` file and replace the Firebase initialization code shown below with the configuration code you got in the previous step: 11 | 12 | 13 | 14 | 21 | 22 | 23 | - In the `index.js` file replace the Realtime demo application key (K4xqxB) with your own Realtime application key: 24 | 25 | var RealtimeAppKey = "K4xqxB"; 26 | 27 | - Edit the `service-worker.js` file enter your Firebase Sender ID in the `messagingSenderId` property: 28 | 29 | firebase.initializeApp({ 30 | 'messagingSenderId': '915139563807' 31 | }); 32 | 33 | - Map a webserver to folder where you have cloned this repository, open http://localhost/index.html in your Chrome/Firefox browser and try it out. If it doesn't work as expected have a look at the limitations and troubleshooting sections below. 34 | 35 | 36 | ## Limitations 37 | 1. This will only work on Chrome 50+ and Firefox 44+ 38 | 2. If you are not using localhost you must use the https protocol (it will work on localhost with http) 39 | 3. At least one Chrome/Firefox tab must be opened in order to receive push notifications 40 | 41 | ## Troubleshooting 42 | 43 | * If you get the following error message it means you have changed the `gcm_sender_id` in your manifest.json file. Please update your manifest and enter the exact value shown in the message: 44 | 45 | Messaging: Please change your web app manifest's 'gcm_sender_id' value to '103953800507' to use Firebase messaging. (messaging/incorrect-gcm-sender-id). 46 | 47 | ### Not receiving push notifications 48 | * Check that you are running the example from a webserver (e.g. http://localhost) and not from the file system (e.g. file:///C:/web/WebPushNotifications-master/index.html); 49 | 50 | * Check that you have entered the right Firebase configurations; 51 | 52 | * Don't forget to give permissions for the push notifications when your browser requests them; 53 | 54 | * Make sure your webserver is properly configured to serve the file manifest.json (check if there are no 404 errors in your browsers Developers Tool network tab). IIS users may need to add the MIME type; 55 | 56 | * If you're not using localhost make sure you are using the https protocol with a valid SSL certificate for the domain you are using; 57 | 58 | * Check if you have any other browser tab opened using the website you're testing. If you do, make sure that page has a Realtime connection established and is subscribing the push notification channel. Push notifications won't be displayed to users that are currently browsing the site that originated the push. 59 | 60 | ## Private channel vs Global channel 61 | If you want to control to which users you are sending each push you should use a private channel for each user. If you want to broadcast a push notification to all users you should use a global channel that every user subscribes. 62 | 63 | A mixed private/global channel strategy can also be used, it really depends on your use case. 64 | 65 | ## On-line example 66 | You can test the Realtime Web Push Notifications [here](https://framework.realtime.co/demo/web-push). 67 | -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | // Give the service worker access to Firebase Messaging. 2 | // Note that you can only use Firebase Messaging here, other Firebase libraries 3 | // are not available in the service worker. 4 | importScripts('https://www.gstatic.com/firebasejs/3.5.0/firebase-app.js'); 5 | importScripts('https://www.gstatic.com/firebasejs/3.5.0/firebase-messaging.js'); 6 | 7 | // Initialize the Firebase app in the service worker by passing in the 8 | // messagingSenderId. 9 | firebase.initializeApp({ 10 | 'messagingSenderId': '915139563807' 11 | }); 12 | 13 | // Retrieve an instance of Firebase Messaging so that it can handle background 14 | // messages. 15 | const fb_messaging = firebase.messaging(); 16 | 17 | // Buffer to save multipart messages 18 | var messagesBuffer = {}; 19 | 20 | // Gets the number of keys in a dictionary 21 | var countKeys = function (dic) { 22 | var count = 0; 23 | for (var i in dic) { 24 | count++; 25 | } 26 | return count; 27 | }; 28 | 29 | // Parses the Realtime messages using multipart format 30 | var parseRealtimeMessage = function (message) { 31 | // Multi part 32 | var regexPattern = /^(\w[^_]*)_{1}(\d*)-{1}(\d*)_{1}([\s\S.]*)$/; 33 | var match = regexPattern.exec(message); 34 | 35 | var messageId = null; 36 | var messageCurrentPart = 1; 37 | var messageTotalPart = 1; 38 | var lastPart = false; 39 | 40 | if (match && match.length > 0) { 41 | if (match[1]) { 42 | messageId = match[1]; 43 | } 44 | if (match[2]) { 45 | messageCurrentPart = match[2]; 46 | } 47 | if (match[3]) { 48 | messageTotalPart = match[3]; 49 | } 50 | if (match[4]) { 51 | message = match[4]; 52 | } 53 | } 54 | 55 | if (messageId) { 56 | if (!messagesBuffer[messageId]) { 57 | messagesBuffer[messageId] = {}; 58 | } 59 | messagesBuffer[messageId][messageCurrentPart] = message; 60 | if (countKeys(messagesBuffer[messageId]) == messageTotalPart) { 61 | lastPart = true; 62 | } 63 | } 64 | else { 65 | lastPart = true; 66 | } 67 | 68 | if (lastPart) { 69 | if (messageId) { 70 | message = ""; 71 | 72 | // Aggregate all parts 73 | for (var i = 1; i <= messageTotalPart; i++) { 74 | message += messagesBuffer[messageId][i]; 75 | delete messagesBuffer[messageId][i]; 76 | } 77 | 78 | delete messagesBuffer[messageId]; 79 | } 80 | 81 | return message; 82 | } else { 83 | // We don't have yet all parts, we need to wait ... 84 | return null; 85 | } 86 | } 87 | 88 | // Shows a notification 89 | function showNotification(message) { 90 | // In this example we are assuming the message is a simple string 91 | // containing the notification text. The target link of the notification 92 | // click is fixed, but in your use case you could send a JSON message with 93 | // a link property and use it in the click_url of the notification 94 | 95 | // The notification title 96 | const notificationTitle = 'Web Push Notification'; 97 | 98 | // The notification properties 99 | const notificationOptions = { 100 | body: message, 101 | icon: 'img/realtime-logo.jpg', 102 | data: { 103 | click_url: '/index.html' 104 | }, 105 | tag: Date.now() 106 | }; 107 | 108 | return self.registration.showNotification(notificationTitle, 109 | notificationOptions); 110 | } 111 | 112 | // If you would like to customize notifications that are received in the 113 | // background (Web app is closed or not in browser focus) then you should 114 | // implement this optional method. 115 | fb_messaging.setBackgroundMessageHandler(function(payload) { 116 | console.log('Received background message ', payload); 117 | 118 | // Customize notification here 119 | if(payload.data && payload.data.M) { 120 | var message = parseRealtimeMessage(payload.data.M); 121 | return showNotification(message); 122 | } 123 | }); 124 | 125 | // Forces a notification 126 | self.addEventListener('message', function (evt) { 127 | evt.waitUntil(showNotification(evt.data)); 128 | }); 129 | 130 | // The user has clicked on the notification ... 131 | self.addEventListener('notificationclick', function(event) { 132 | // Android doesn’t close the notification when you click on it 133 | // See: http://crbug.com/463146 134 | event.notification.close(); 135 | 136 | if(event.notification.data && event.notification.data.click_url) { 137 | // gets the notitication click url 138 | var click_url = event.notification.data.click_url; 139 | 140 | // This looks to see if the current is already open and 141 | // focuses if it is 142 | event.waitUntil(clients.matchAll({ 143 | type: "window" 144 | }).then(function(clientList) { 145 | for (var i = 0; i < clientList.length; i++) { 146 | var client = clientList[i]; 147 | if (client.url == click_url && 'focus' in client) 148 | return client.focus(); 149 | } 150 | if (clients.openWindow) { 151 | var url = click_url; 152 | return clients.openWindow(url); 153 | } 154 | 155 | })); 156 | } 157 | }); 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Realtime Web Push Notifications example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 42 | 53 | 54 | 55 | 56 | 57 |
58 |
59 |
60 | 61 |

62 | 63 |

64 | 65 |

Introducing Web Push Notifications

66 | 67 |

68 | Starting with Chrome 50 and Firefox 44 you are able to send push notifications to your website users, even when they are not browsing your website. 69 |

70 |

71 | We have integrated this new feature into the Realtime Cloud Messaging Pub/Sub service to make your life easier. 72 |

73 | 74 |

How?

75 | 76 |

77 | You probably noticed that your browser asked if you allowed notifications from this website. 78 | If you accepted you are already subscribed to your own private channel to receive notifications. 79 |

80 | 81 |

82 | Your channel is: ... 83 |

84 | 85 |

86 | This was performed through the new subscribeWithNotifications method in the Realtime JavaScript SDK. 87 |

88 |

89 | Demo time! Clicking on the following button will send a new push notification to your private channel (only you'll see the push). 90 |

91 | 92 |

93 | 94 |

95 | 96 |

97 | You should see a Chrome or Firefox Push Notification pop-up like this near your system tray (the first push may take a little more time to be delivered due to the underlying Firebase Cloud Messaging backend configuration). 98 |

99 |

100 | 101 |

102 | 103 |

104 | 105 |

106 | or 107 |

108 | 109 |

110 | 111 |

112 | 113 |

114 | This example uses a private channel for each user so you can send pushes at will, without annoying other users. A typical use case in a website would be using a global channel and subscribing each user to that channel. This way you could broadcast a single push to all users simultaneously. It´s powerful but should be used wisely. 115 |

116 | 117 |
118 |
119 | 120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 | 128 |

Cool! What if the user is not browsing the website when the push notification is sent?

129 | 130 |

131 | No problem, if an instance of Chrome or Firefox is running users will see your notification pop-up inviting them back to your website. If Chrome or Firefox is not running the pop-up will be shown as soon as the browser is launched. 132 |

133 | 134 |

135 | Let's try it. Copy the following curl command and navigate away from this website. Paste the command in your terminal window and send yourself a push notification. When you receive it click on the pop-up and you'll be redirected to the Realtime homepage. 136 |

137 | 138 |

139 |

140 |
141 |

142 | 143 |

144 | The command above is simply sending a Realtime message to your private channel (the C parameter) using the Realtime REST API. The Realtime server will take care of broadcasting it to Firebase Cloud Messaging for delivery. 145 |

146 | 147 |

One push, many platforms

148 |

149 | But the good news won't stop here. Taking advantage of the Realtime Cloud Messaging integration with Firebase Cloud Messaging for Android devices and Apple Notification Services for iOS/OSX devices, now you'll be able to send one single push to your entire user base in almost all platforms. Until now you could only do it with native mobile apps, now you can do it also for your website users. 150 |

151 | 152 | 153 |

This is great! How can I get started?

154 |

155 | You'll find this example source code and starting guide at https://github.com/realtime-framework/WebPushNotifications 156 |

157 | 158 |

159 | Feel free to clone it and run it in your localhost or website. 160 |

161 | 162 |

163 | Follow the guidelines and in less than one hour you'll be engaging with your website users. Go for it ... 164 |

165 | 166 |
167 |
168 |
169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | --------------------------------------------------------------------------------