├── .gitignore ├── History.md ├── LICENSE.md ├── README.md ├── app ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── cordova-plugins │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── client │ ├── accounts │ │ ├── accounts-styling.css │ │ ├── accounts.js │ │ └── accounts.less │ ├── blog │ │ ├── blog.html │ │ ├── blog.less │ │ ├── hljs.css │ │ └── shareit │ │ │ ├── shareit-config.js │ │ │ └── shareit.less │ ├── constants.import.less │ ├── index.html │ ├── pure-0.5.0 │ │ ├── pure-min-with-grid.css │ │ └── purify.js │ ├── route.js │ ├── site.js │ ├── site.less │ └── startup.js ├── gs-cors.json ├── i18n │ ├── en.i18n.json │ └── fr.i18n.json ├── lib │ └── slingshot-restrictions.js ├── packages │ └── src ├── private │ ├── .gitignore │ ├── publish_email_en.html │ └── publish_email_fr.html ├── project-tap.i18n ├── public │ ├── favicon.ico │ └── img │ │ ├── common │ │ └── file-icons.png │ │ └── markdown.png ├── server │ ├── emails.js │ ├── slingshot.js │ └── users.js ├── settings-meteor-com.json └── settings.json └── src ├── .gitignore ├── LICENSE.md ├── README.md ├── client ├── blog-client.js ├── blog-picture.js ├── blog-route.js ├── blog-templates.html └── blog.less ├── common ├── blog-collections.js └── blog-paths.js ├── doc └── dragdrop.png ├── i18n ├── en.i18n.json └── fr.i18n.json ├── package-tap.i18n ├── package.js ├── server └── blog-server.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .idea -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.5.2 2 | ===== 3 | 4 | * settings file now only required on server in production (Issue #32) 5 | 6 | 0.5.1 7 | ===== 8 | 9 | * Fixes settings not being recognized issue 10 | 11 | 0.5.0 12 | ===== 13 | 14 | * Support for uploding and inserting pictures (Issue #20) using `edgee:slingshot`. 15 | * Fixed a few jshint warnings 16 | 17 | 0.4.1 18 | ===== 19 | 20 | * Option to send an email to all registered users when a new post is published. 21 | * Fixed date format in French 22 | 23 | 0.4.0 24 | ===== 25 | 26 | * Moment js now configured in i18n bundles. Out-of-the-box support for English (default) and French. 27 | * Locale can be changed reactively using `Session.set("locale", newLocale)` 28 | 29 | 0.3.9 30 | ===== 31 | 32 | * Full support for i18n. Blog in one language at a time. Out-of-the-box support for English (default) and French. 33 | 34 | 0.3.8 35 | ===== 36 | 37 | * Blog posts' date can now be displayed in locales other than English (Issue #14) 38 | 39 | 0.3.7 40 | ===== 41 | 42 | * shortId is now configurable and doesn't have to be included in the URL 43 | * blog and archive path can now be customized through the settings.json 44 | 45 | pre 0.3.7 46 | ========= 47 | 48 | Murky ancient history 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | Copyright Sam Hatoum, Xolv.io 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ``` 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # - - DEPRECATED - - 2 | 3 | xolvio:md-blog 4 | ============== 5 | 6 | This is the blog package currently used on The Meteor Testing Manual. It will give you a 7 | markdown powered blog on your site. 8 | 9 | * WYSIWYG Markdown support with in-place editing 10 | * Syntax highlighting using [highlight.js](https://highlightjs.org/) 11 | * Customizable styling with the ability to add your own classes to elements! 12 | * Publish / Unpublish / Archive / Unarchive workflows 13 | * I18n Support: the blog engine can be configured to work in any language 14 | * Send email to registered users when a new post is published 15 | * Click on the "Insert Pictures" button to choose pictures, or Drag & Drop pictures into the editing area, and they will be uploaded and Markdown links created.
![Drag & drop](https://raw.githubusercontent.com/xolvio/md-blog/master/src/doc/dragdrop.png) 16 | 17 | 18 | [Try the demo site](http://md-blog.meteor.com) 19 | 20 | [See the demo site code](https://github.com/xolvio/md-blog) 21 | 22 | ##Installation 23 | 24 | `> meteor add xolvio:md-blog` 25 | 26 | ##Getting Started 27 | 28 | There are a couple of steps to carry out before you can start using this package. Don't worry 29 | it's very easy! 30 | 31 | ### 1. Define Layouts 32 | First you need to define two templates that the blog in your app that look like this: 33 | 34 | ```html 35 | 38 | 39 | 42 | ``` 43 | 44 | These are iron-router layouts that you be available at `/blog` and `/blog/:_id/:slug`. 45 | 46 | You may want to customize these template further like adding disqus for instance: 47 | 48 | ```html 49 | 53 | ``` 54 | 55 | ### 2. Setup Accounts & Roles 56 | You need to ensure you are using a Meteor accounts package like accounts-password, and that the 57 | user you are logged has 'roles' array with the element `mdblog-author`. Here's an example of a 58 | user object: 59 | 60 | ```json 61 | { 62 | "_id": "ixoreoJY5wzmNYMcY", 63 | "emails": [ { 64 | "address" : "sam@xolv.io", 65 | "verified" : true 66 | } ], 67 | "profile": { "name" : "Sam Hatoum" }, 68 | "roles": [ "mdblog-author" ] 69 | } 70 | ``` 71 | 72 | You can also the above pragmatically by calling 73 | `Roles.addUsersToRoles(user._id, ['mdblog-author']);` 74 | 75 | For more information about roles, have a look at the 76 | [alanning:roles](https://github.com/alanning/meteor-roles) package. 77 | 78 | 79 | ### 3. Add the `tap:i18n` package 80 | 81 | I18n is built-in and uses `tap:i18n`; as a result, you need to `meteor add tap:i18n` even if your website is English only. 82 | 83 | 84 | ### 4. Customize 85 | 86 | This blog is designed to be fully customizable and as unopinionated as possible. Here are some of the ways you can configure it. 87 | 88 | 89 | ####Styling 90 | 91 | To style the blog list and posts, apply css 92 | [just like in the demo app](https://github.com/xolvio/md-blog/blob/master/app/client/blog/blog.less). 93 | 94 | For syntax highlighting style, you need to add the hljs css file of your choice. 95 | [Pick a css template from here](https://highlightjs.org/static/demo/). You can see this in the 96 | demo app, 97 | [there is a file named hljs.css](https://github.com/xolvio/md-blog/tree/master/app/client/blog). 98 | 99 | ####Custom Classes 100 | You can also add classes to any element of your choice! For this you need to use the settings.json 101 | file. Have a look at the settings.json file below. You can see there's a field named 102 | `element-classes`. The example above is adding the class 103 | `pure-img` to all `img` elements. This is very powerful as it allows you to use your CSS 104 | framework of your choice. 105 | 106 | ####Sorting 107 | By default, the blog sorts your posts by date. You can change this by modifying the `sortBy` 108 | field in the settings file. 109 | 110 | ####Blog Routes 111 | 112 | The blog runs at the default "/blog" route. The archive runs at the default 113 | "/blog/archive" route. For each post, the default is the "blog/:shortId/:slug" route. You can customize where the blog handles requests by 114 | changing the `blogPath` and `archivePath`. You can also remove the short id from the blog post path by setting the `useUniqueBlogPostsPath` to false. 115 | 116 | ####Settings File Example 117 | ```json 118 | { 119 | "public": { 120 | "blog": { 121 | "name": "The Xolv.io md-blog", 122 | "Description": "Get verbal on your websites.", 123 | "prettify": { 124 | "syntax-highlighting": true, 125 | "element-classes": [ 126 | { 127 | "locator": "img", 128 | "classes": ["pure-img"] 129 | }, 130 | { 131 | "locator": "button", 132 | "classes": ["pure-button"] 133 | } 134 | ] 135 | }, 136 | "sortBy": {"date": -1}, 137 | "blogPath": "/blog", 138 | "archivePath": "/blog/archive", 139 | "useUniqueBlogPostsPath": true 140 | } 141 | } 142 | } 143 | ``` 144 | 145 | ####i18n 146 | The blog engine can be configured to display messages and button texts in any language. 147 | English is the default language, and translations are provided for the French language. 148 | Specify `defaultLocale` at the same level as the blog name, in the `settings.json` file: 149 | 150 | ```json 151 | { 152 | "public": { 153 | "blog": { 154 | ... 155 | "defaultLocale": "fr" 156 | } 157 | } 158 | } 159 | ``` 160 | **How do I change the language depending on my user's preference?** 161 | 162 | Simply call `Session.set('locale', newLocale)` 163 | 164 | **I need translations for a new language!** 165 | - Refer to the `tap:i18n` package [documentation](https://github.com/TAPevents/tap-i18n#documentation--examples). You should take a look at the sample app first. 166 | - There are two ways to provide additional languages: 1. the preferred way is to submit a Pull Request to integrate the new `i18n/.i18n.json`. 2. The other way is to place this file in your Meteor application. 167 | - When adding a new language, you will also want to configure Moment to display the localized version of `today at hh:mm` and other such texts. This is done through the `moment` object in the i18n bundle (`i18n/language.i18n.json`). 168 | The following example sets the days and months in French, and configures a few moments in French as well. 169 | (Note that this is not comprehensive. Refer to the [Moment documentation](http://momentjs.com/docs/#/i18n/changing-locale/) for more settings.) 170 | Due to `tap:i18n` only supporting Strings and not Objects in its bundles, you have to use a JSON string. The JSON string may be broken down into an array of Strings for better readability. 171 | 172 | As an example, in order to get: 173 | ```json 174 | "moment": { 175 | "weekdays": [ "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche" ], 176 | "calendar": { 177 | "lastDay": "[hier à] LT", 178 | "sameDay": "[aujourd'hui à] LT", 179 | "nextDay": "[demain à] LT", 180 | "lastWeek": "[dernier] dddd [à] LT", 181 | "nextWeek": "dddd [à] LT", 182 | "sameElse": "DD/MM/YYYY" 183 | } 184 | } 185 | ``` 186 | You need to write: 187 | `// in app/i18n/language.i18n.json:` 188 | ```json 189 | "moment": [ 190 | "{", 191 | "\"weekdays\": [ \"lundi\", \"mardi\", \"mercredi\", \"jeudi\", \"vendredi\", \"samedi\", \"dimanche\" ],", 192 | "\"calendar\": {", 193 | "\"lastDay\": \"[hier à] LT\",", 194 | "\"sameDay\": \"[aujourd'hui à] LT\",", 195 | "\"nextDay\": \"[demain à] LT\",", 196 | "\"lastWeek\": \"dddd [dernier à] LT\",", 197 | "\"nextWeek\": \"dddd [à] LT\",", 198 | "\"sameElse\": \"DD/MM/YYYY\"", 199 | "}", 200 | "}" 201 | ] 202 | ``` 203 | 204 | ####Send an email when a new post is published 205 | 1. Set `emailOnPublish` to true: 206 | ```json 207 | { 208 | "public": { 209 | "blog": { 210 | ... 211 | "emailOnPublish": "true" 212 | } 213 | } 214 | } 215 | ``` 216 | 2. Run `meteor add meteorhacks:ssr` to add the SSR package. 217 | 3. Provide a compiled template named `publishEmail`. For example, if the `publish_email.html` template sits in the `private` directory, make sure to run: 218 | ``` 219 | SSR.compileTemplate('publishEmail', 220 | Assets.getText('publish_email.html')); 221 | ``` 222 | 223 | An email will be sent to all registered users (in Bcc) when a post is published. Its sender will be the post's author. Its title will be the post's title. Its body will come from the `publishEmail` template. The template should contain the blog post's `summary` and its `url`. Example: 224 | ``` 225 |

{{summary}}

226 | Read more... 227 | ``` 228 | 229 | ####Configure pictures max size and Slingshot directive 230 | To understand how to configure `edgee:slingshot`, read [its documentation](https://github.com/CulturalMe/meteor-slingshot/#aws-s3). 231 | ```json 232 | { 233 | "public": { 234 | "blog": { 235 | ... 236 | "pictures": { 237 | "maxWidth": "800", 238 | "maxHeight": "800", 239 | "Slingshot": { 240 | "directive": "" 241 | } 242 | } 243 | } 244 | } 245 | } 246 | ``` 247 | The demo app will work with Google Cloud Storage if you provide a `pem` file and a file that contains the `GoogleAccessId` as Assets (under the `private` directory).: 248 | ```json 249 | { 250 | "public": { 251 | "blog": { 252 | ... 253 | "pictures": { 254 | "Slingshot": { 255 | "pemFile": "google-cloud-service-key.pem", 256 | "idTextFile": "google-cloud-access-id.txt" 257 | } 258 | } 259 | } 260 | } 261 | } 262 | ``` 263 | 264 | 265 | ##Additional Info 266 | 267 | ###URL Format 268 | The URL format of your blog will look like this: 269 | 270 | `www.your-site.com/blog` 271 | 272 | `www.your-site.com/blog/7yh22/your-latest-blog-post` 273 | 274 | The format is `/:_id/:slug` 275 | 276 | The `:slug` is the title of the blog post with all the spaces replaced with dashes. It's believed 277 | this is good for SEO purposes. 278 | 279 | The `:_id` is a truncated version of the mongo id for the blog entry. This allows you to have 280 | multiple posts with the same title over time. 281 | 282 | When you archive blog posts, currently they are removed from the main view but they are still 283 | accessible by search engines and external links. To see your archived blog entries, go to: 284 | 285 | `www.your-site.com/archive` 286 | 287 | ###Environment Variables 288 | 289 | If you want the app to delete all the blog entries on startup, set the environment variable 290 | `AUTO_RESET=1` when running meteor. For example: 291 | 292 | ```bash 293 | AUTO_RESET=1 meteor 294 | ``` 295 | 296 | ##Contribution 297 | Yes please! 298 | 299 | Todo list: 300 | * [ ] Your idea! 301 | * [ ] Date Picker 302 | * [ ] Author Picker 303 | * [ ] Pagination 304 | * [ ] Auto draft saving + history 305 | -------------------------------------------------------------------------------- /app/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /app/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | iwgw2lhn0eqtvknvd2 8 | -------------------------------------------------------------------------------- /app/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | meteor-platform 7 | xolvio:md-blog 8 | iron:router 9 | fortawesome:fontawesome 10 | useraccounts:unstyled 11 | accounts-password 12 | less 13 | joshowens:shareit 14 | tap:i18n 15 | ogourment:settings 16 | meteorhacks:ssr 17 | edgee:slingshot 18 | -------------------------------------------------------------------------------- /app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.4.2 2 | -------------------------------------------------------------------------------- /app/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.0 2 | accounts-password@1.1.0 3 | alanning:roles@1.2.13 4 | aldeed:simple-schema@1.3.0 5 | autoupdate@1.2.0 6 | base64@1.0.3 7 | binary-heap@1.0.3 8 | blaze@2.1.0 9 | blaze-tools@1.0.3 10 | boilerplate-generator@1.0.3 11 | callback-hook@1.0.3 12 | ccorcos:clientside-image-manipulation@1.0.3 13 | cfs:dropped-event@0.0.10 14 | cfs:http-methods@0.0.27 15 | check@1.0.5 16 | chuangbo:marked@0.3.5 17 | coffeescript@1.0.6 18 | ddp@1.1.0 19 | deps@1.0.7 20 | edgee:slingshot@0.6.0 21 | ejson@1.0.6 22 | email@1.0.6 23 | fastclick@1.0.3 24 | fortawesome:fontawesome@4.2.0_2 25 | geojson-utils@1.0.3 26 | html-tools@1.0.4 27 | htmljs@1.0.4 28 | http@1.1.0 29 | id-map@1.0.3 30 | iron:controller@1.0.7 31 | iron:core@1.0.7 32 | iron:dynamic-template@1.0.7 33 | iron:layout@1.0.7 34 | iron:location@1.0.7 35 | iron:middleware-stack@1.0.7 36 | iron:router@1.0.7 37 | iron:url@1.0.7 38 | joshowens:shareit@0.3.1 39 | jquery@1.11.3_2 40 | json@1.0.3 41 | launch-screen@1.0.2 42 | less@1.0.13 43 | livedata@1.0.13 44 | localstorage@1.0.3 45 | logging@1.0.7 46 | meteor@1.1.5 47 | meteor-platform@1.2.2 48 | meteorhacks:ssr@2.1.2 49 | minifiers@1.1.4 50 | minimongo@1.0.7 51 | mobile-status-bar@1.0.3 52 | mongo@1.1.0 53 | mrt:moment@2.8.1 54 | npm-bcrypt@0.7.8_1 55 | observe-sequence@1.0.5 56 | ogourment:settings@0.1.4 57 | ordered-dict@1.0.3 58 | random@1.0.3 59 | reactive-dict@1.1.0 60 | reactive-var@1.0.5 61 | reload@1.1.3 62 | retry@1.0.3 63 | routepolicy@1.0.5 64 | service-configuration@1.0.4 65 | session@1.1.0 66 | sha@1.0.3 67 | softwarerero:accounts-t9n@1.0.6 68 | spacebars@1.0.6 69 | spacebars-compiler@1.0.5 70 | spiderable@1.0.7 71 | srp@1.0.3 72 | tap:i18n@1.4.1 73 | templating@1.1.0 74 | tracker@1.0.6 75 | ui@1.0.6 76 | underscore@1.0.3 77 | url@1.0.4 78 | useraccounts:core@1.8.1 79 | useraccounts:unstyled@1.8.1 80 | webapp@1.2.0 81 | webapp-hashing@1.0.3 82 | xolvio:hljs@0.0.1 83 | xolvio:md-blog@0.5.2 84 | -------------------------------------------------------------------------------- /app/client/accounts/accounts-styling.css: -------------------------------------------------------------------------------- 1 | #at-nav-button { 2 | cursor: pointer; 3 | } -------------------------------------------------------------------------------- /app/client/accounts/accounts.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | AccountsTemplates.configureRoute('signUp'); 4 | AccountsTemplates.configureRoute('signIn'); 5 | 6 | AccountsTemplates.configure({ 7 | // Behaviour 8 | confirmPassword: true, 9 | enablePasswordChange: true, 10 | forbidClientAccountCreation: false, 11 | overrideLoginErrors: true, 12 | sendVerificationEmail: false, 13 | 14 | // Appearance 15 | showAddRemoveServices: false, 16 | showForgotPasswordLink: true, 17 | showLabels: true, 18 | showPlaceholders: true, 19 | 20 | // Client-side Validation 21 | continuousValidation: false, 22 | negativeFeedback: false, 23 | negativeValidation: true, 24 | positiveValidation: true, 25 | positiveFeedback: true, 26 | showValidating: true, 27 | 28 | // Privacy Policy and Terms of Use 29 | //privacyUrl: 'privacy', 30 | //termsUrl: 'terms-of-use', 31 | 32 | // Redirects 33 | homeRoutePath: '/', 34 | redirectTimeout: 4000, 35 | 36 | // Texts 37 | texts: { 38 | button: { 39 | signUp: "Register Now!" 40 | }, 41 | socialSignUp: "Register", 42 | socialIcons: { 43 | "meteor-developer": "fa fa-rocket" 44 | }, 45 | title: { 46 | forgotPwd: "Recover Your Password" 47 | } 48 | } 49 | }); 50 | 51 | })(); 52 | -------------------------------------------------------------------------------- /app/client/accounts/accounts.less: -------------------------------------------------------------------------------- 1 | @import '../constants.import.less'; 2 | 3 | .pure-form input:focus { 4 | outline: 0; 5 | outline: thin dotted \9; 6 | border-color: @xolvio-orange !important; 7 | } 8 | 9 | .at-form { 10 | input { 11 | width: 60% !important; 12 | } 13 | label { 14 | display: inline-block; 15 | width: 30% !important; 16 | } 17 | button[type="submit"] { 18 | float: right; 19 | } 20 | width: 50%; 21 | margin: 7em auto; 22 | } -------------------------------------------------------------------------------- /app/client/blog/blog.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /app/client/blog/blog.less: -------------------------------------------------------------------------------- 1 | @import '../constants.import.less'; 2 | 3 | .blog-controls { 4 | width: inherit; 5 | button { 6 | background-color: @xolvio-grey-03; 7 | &#mdblog-new { 8 | color: white; 9 | float: right; 10 | } 11 | } 12 | } 13 | 14 | .blog-list { 15 | 16 | width: 90%; 17 | margin: 120px auto; 18 | 19 | .blog-post { 20 | 21 | margin-bottom: 40px; 22 | border-bottom: 1px solid #ccc; 23 | 24 | header { 25 | h2 { 26 | text-transform: uppercase; 27 | border: none; 28 | } 29 | summary { 30 | font-style: italic; 31 | } 32 | .info { 33 | .author { 34 | } 35 | time { 36 | } 37 | } 38 | } 39 | summary { 40 | } 41 | } 42 | } 43 | 44 | .blog-post { 45 | 46 | .blog-controls { 47 | margin-top: -30px; 48 | } 49 | 50 | width: 80%; 51 | margin: 0 auto; 52 | header { 53 | h1 { 54 | font-family: 'Roboto Condensed', sans-serif; 55 | font-size: 3em; 56 | margin-bottom: 1.5em; 57 | color: #666; 58 | text-transform: uppercase; 59 | line-height: 1em; 60 | } 61 | #summary { 62 | font-style: italic; 63 | } 64 | .info { 65 | color: #333; 66 | 67 | .author { 68 | font-weight: bold; 69 | color: #999; 70 | } 71 | time { 72 | font-style: italic; 73 | } 74 | } 75 | } 76 | article { 77 | h1 { 78 | color: #666; 79 | } 80 | } 81 | } 82 | 83 | // The styling below is only required by this demo app 84 | 85 | #instructions { 86 | position: absolute; 87 | width: 100%; 88 | text-align: center; 89 | top: 65px; 90 | @media (min-width: 48em) { 91 | top: 40px; 92 | } 93 | color: @xolvio-orange; 94 | text-transform: uppercase; 95 | 96 | } 97 | 98 | @-webkit-keyframes blinker { 99 | 0% { opacity: 0.0; } 100 | 10% { opacity: 1.0; } 101 | 90% { opacity: 1.0; } 102 | 100% { opacity: 0.0; } 103 | } 104 | .blink { 105 | -webkit-animation-name: blinker; 106 | animation-direction: alternate; 107 | 108 | -webkit-animation-iteration-count: infinite; 109 | animation-timing-function: ease-out; 110 | -webkit-animation-duration: 5s; 111 | } 112 | -------------------------------------------------------------------------------- /app/client/blog/hljs.css: -------------------------------------------------------------------------------- 1 | .hljs { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 0.5em; 5 | background: #3f3f3f; 6 | color: #dcdcdc; 7 | -webkit-text-size-adjust: none; 8 | } 9 | 10 | .hljs-keyword, 11 | .hljs-tag, 12 | .css .hljs-class, 13 | .css .hljs-id, 14 | .lisp .hljs-title, 15 | .nginx .hljs-title, 16 | .hljs-request, 17 | .hljs-status, 18 | .clojure .hljs-attribute { 19 | color: #e3ceab; 20 | } 21 | 22 | .django .hljs-template_tag, 23 | .django .hljs-variable, 24 | .django .hljs-filter .hljs-argument { 25 | color: #dcdcdc; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-date { 30 | color: #8cd0d3; 31 | } 32 | 33 | .dos .hljs-envvar, 34 | .dos .hljs-stream, 35 | .hljs-variable, 36 | .apache .hljs-sqbracket { 37 | color: #efdcbc; 38 | } 39 | 40 | .dos .hljs-flow, 41 | .diff .hljs-change, 42 | .python .exception, 43 | .python .hljs-built_in, 44 | .hljs-literal, 45 | .tex .hljs-special { 46 | color: #efefaf; 47 | } 48 | 49 | .diff .hljs-chunk, 50 | .hljs-subst { 51 | color: #8f8f8f; 52 | } 53 | 54 | .dos .hljs-keyword, 55 | .hljs-decorator, 56 | .hljs-title, 57 | .hljs-type, 58 | .diff .hljs-header, 59 | .ruby .hljs-class .hljs-parent, 60 | .apache .hljs-tag, 61 | .nginx .hljs-built_in, 62 | .tex .hljs-command, 63 | .hljs-prompt { 64 | color: #efef8f; 65 | } 66 | 67 | .dos .hljs-winutils, 68 | .ruby .hljs-symbol, 69 | .ruby .hljs-symbol .hljs-string, 70 | .ruby .hljs-string { 71 | color: #dca3a3; 72 | } 73 | 74 | .diff .hljs-deletion, 75 | .hljs-string, 76 | .hljs-tag .hljs-value, 77 | .hljs-preprocessor, 78 | .hljs-pragma, 79 | .hljs-built_in, 80 | .hljs-javadoc, 81 | .smalltalk .hljs-class, 82 | .smalltalk .hljs-localvars, 83 | .smalltalk .hljs-array, 84 | .css .hljs-rules .hljs-value, 85 | .hljs-attr_selector, 86 | .hljs-pseudo, 87 | .apache .hljs-cbracket, 88 | .tex .hljs-formula, 89 | .coffeescript .hljs-attribute { 90 | color: #cc9393; 91 | } 92 | 93 | .hljs-shebang, 94 | .diff .hljs-addition, 95 | .hljs-comment, 96 | .hljs-annotation, 97 | .hljs-template_comment, 98 | .hljs-pi, 99 | .hljs-doctype { 100 | color: #7f9f7f; 101 | } 102 | 103 | .coffeescript .javascript, 104 | .javascript .xml, 105 | .tex .hljs-formula, 106 | .xml .javascript, 107 | .xml .vbscript, 108 | .xml .css, 109 | .xml .hljs-cdata { 110 | opacity: 0.5; 111 | } 112 | 113 | -------------------------------------------------------------------------------- /app/client/blog/shareit/shareit-config.js: -------------------------------------------------------------------------------- 1 | ShareIt.configure({ 2 | useFB: true, // boolean (default: true) 3 | // Whether to show the Facebook button 4 | useTwitter: true, // boolean (default: true) 5 | // Whether to show the Twitter button 6 | useGoogle: true, // boolean (default: true) 7 | // Whether to show the Google+ button 8 | classes: "large btn", // string (default: 'large btn') 9 | // The classes that will be placed on the sharing buttons, bootstrap by default. 10 | iconOnly: true, // boolean (default: false) 11 | // Don't put text on the sharing buttons 12 | applyColors: true // boolean (default: true) 13 | // apply classes to inherit each social networks background color 14 | }); -------------------------------------------------------------------------------- /app/client/blog/shareit/shareit.less: -------------------------------------------------------------------------------- 1 | .share-wrapper { 2 | margin: 120px auto 0; 3 | .share-buttons { 4 | width: 80%; 5 | .btn { 6 | display: block; 7 | width: 32px; 8 | height: 32px; 9 | padding: 5px; 10 | margin-left: 10px; 11 | text-align: center; 12 | float: right; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /app/client/constants.import.less: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Roboto:400italic,400|Roboto+Condensed); 2 | 3 | @xolvio-color: rgba(86, 191, 152, 1); 4 | @xolvio-grey-01: rgba(40, 40, 40, 1); 5 | @xolvio-grey-02: rgba(109, 117, 121, 1); 6 | @xolvio-grey-03: rgba(176, 182, 184, 1); 7 | @xolvio-grey-04: rgba(213, 217, 216, 1); 8 | @xolvio-red: rgba(255, 100, 100, 1); 9 | @xolvio-white: rgba(255, 255, 255, 1); 10 | @xolvio-black: rgba(0, 0, 0, 1); 11 | @xolvio-orange: #df7514; 12 | 13 | .transition (@transition) { 14 | -webkit-transition: @transition; 15 | -moz-transition: @transition; 16 | -ms-transition: @transition; 17 | -o-transition: @transition; 18 | transition: @transition; 19 | } -------------------------------------------------------------------------------- /app/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 30 | 31 | 45 | 46 | 110 | -------------------------------------------------------------------------------- /app/client/pure-0.5.0/pure-min-with-grid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v0.5.0 3 | Copyright 2014 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | https://github.com/yui/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v1.1.3 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v1.1.3 | MIT License | git.io/normalize */ 12 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;*font-size:90%;*overflow:visible;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);*color:#444;border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin dotted #333;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#ee5f5b}.pure-form input:focus:invalid:focus,.pure-form textarea:focus:invalid:focus,.pure-form select:focus:invalid:focus{border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 10em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input{display:block;padding:10px;margin:0;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus{z-index:2}.pure-form .pure-group input:first-child{top:1px;border-radius:4px 4px 0 0}.pure-form .pure-group input:last-child{top:-2px;border-radius:0 0 4px 4px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu ul{position:absolute;visibility:hidden}.pure-menu.pure-menu-open{visibility:visible;z-index:2;width:100%}.pure-menu ul{left:-10000px;list-style:none;margin:0;padding:0;top:-10000px;z-index:1}.pure-menu>ul{position:relative}.pure-menu-open>ul{left:0;top:0;visibility:visible}.pure-menu-open>ul:focus{outline:0}.pure-menu li{position:relative}.pure-menu a,.pure-menu .pure-menu-heading{display:block;color:inherit;line-height:1.5em;padding:5px 20px;text-decoration:none;white-space:nowrap}.pure-menu.pure-menu-horizontal>.pure-menu-heading{display:inline-block;*display:inline;zoom:1;margin:0;vertical-align:middle}.pure-menu.pure-menu-horizontal>ul{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu li a{padding:5px 20px}.pure-menu-can-have-children>.pure-menu-label:after{content:'\25B8';float:right;font-family:'Lucida Grande','Lucida Sans Unicode','DejaVu Sans',sans-serif;margin-right:-20px;margin-top:-1px}.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-separator{background-color:#dfdfdf;display:block;height:1px;font-size:0;margin:7px 2px;overflow:hidden}.pure-menu-hidden{display:none}.pure-menu-fixed{position:fixed;top:0;left:0;width:100%}.pure-menu-horizontal li{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-horizontal li li{display:block}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label:after{content:"\25BE"}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-horizontal li.pure-menu-separator{height:50%;width:1px;margin:0 7px}.pure-menu-horizontal li li.pure-menu-separator{height:1px;width:auto;margin:7px 2px}.pure-menu.pure-menu-open,.pure-menu.pure-menu-horizontal li .pure-menu-children{background:#fff;border:1px solid #b7b7b7}.pure-menu.pure-menu-horizontal,.pure-menu.pure-menu-horizontal .pure-menu-heading{border:0}.pure-menu a{border:1px solid transparent;border-left:0;border-right:0}.pure-menu a,.pure-menu .pure-menu-can-have-children>li:after{color:#777}.pure-menu .pure-menu-can-have-children>li:hover:after{color:#fff}.pure-menu .pure-menu-open{background:#dedede}.pure-menu li a:hover,.pure-menu li a:focus{background:#eee}.pure-menu li.pure-menu-disabled a:hover,.pure-menu li.pure-menu-disabled a:focus{background:#fff;color:#bfbfbf}.pure-menu .pure-menu-disabled>a{background-image:none;border-color:transparent;cursor:default}.pure-menu .pure-menu-disabled>a,.pure-menu .pure-menu-can-have-children.pure-menu-disabled>a:after{color:#bfbfbf}.pure-menu .pure-menu-heading{color:#565d64;text-transform:uppercase;font-size:90%;margin-top:.5em;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#dfdfdf}.pure-menu .pure-menu-selected a{color:#000}.pure-menu.pure-menu-open.pure-menu-fixed{border:0;border-bottom:1px solid #b7b7b7}.pure-paginator{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;list-style:none;margin:0;padding:0}.opera-only :-o-prefocus,.pure-paginator{word-spacing:-.43em}.pure-paginator li{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-paginator .pure-button{border-radius:0;padding:.8em 1.4em;vertical-align:top;height:1.1em}.pure-paginator .pure-button:focus,.pure-paginator .pure-button:active{outline-style:none}.pure-paginator .prev,.pure-paginator .next{color:#C0C1C3;text-shadow:0 -1px 0 rgba(0,0,0,.45)}.pure-paginator .prev{border-radius:2px 0 0 2px}.pure-paginator .next{border-radius:0 2px 2px 0}@media (max-width:480px){.pure-menu-horizontal{width:100%}.pure-menu-children li{display:block;border-bottom:1px solid #000}}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child td,.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0} 13 | 14 | /* 15 | Grid 16 | */ 17 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%;*width:4.1357%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%;*width:8.3023%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%;*width:12.469%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%;*width:16.6357%}.pure-u-sm-1-5{width:20%;*width:19.969%}.pure-u-sm-5-24{width:20.8333%;*width:20.8023%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%;*width:24.969%}.pure-u-sm-7-24{width:29.1667%;*width:29.1357%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%;*width:33.3023%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%;*width:37.469%}.pure-u-sm-2-5{width:40%;*width:39.969%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%;*width:41.6357%}.pure-u-sm-11-24{width:45.8333%;*width:45.8023%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%;*width:49.969%}.pure-u-sm-13-24{width:54.1667%;*width:54.1357%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%;*width:58.3023%}.pure-u-sm-3-5{width:60%;*width:59.969%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%;*width:62.469%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%;*width:66.6357%}.pure-u-sm-17-24{width:70.8333%;*width:70.8023%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%;*width:74.969%}.pure-u-sm-19-24{width:79.1667%;*width:79.1357%}.pure-u-sm-4-5{width:80%;*width:79.969%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%;*width:83.3023%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%;*width:87.469%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%;*width:91.6357%}.pure-u-sm-23-24{width:95.8333%;*width:95.8023%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%;*width:4.1357%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%;*width:8.3023%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%;*width:12.469%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%;*width:16.6357%}.pure-u-md-1-5{width:20%;*width:19.969%}.pure-u-md-5-24{width:20.8333%;*width:20.8023%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%;*width:24.969%}.pure-u-md-7-24{width:29.1667%;*width:29.1357%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%;*width:33.3023%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%;*width:37.469%}.pure-u-md-2-5{width:40%;*width:39.969%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%;*width:41.6357%}.pure-u-md-11-24{width:45.8333%;*width:45.8023%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%;*width:49.969%}.pure-u-md-13-24{width:54.1667%;*width:54.1357%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%;*width:58.3023%}.pure-u-md-3-5{width:60%;*width:59.969%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%;*width:62.469%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%;*width:66.6357%}.pure-u-md-17-24{width:70.8333%;*width:70.8023%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%;*width:74.969%}.pure-u-md-19-24{width:79.1667%;*width:79.1357%}.pure-u-md-4-5{width:80%;*width:79.969%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%;*width:83.3023%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%;*width:87.469%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%;*width:91.6357%}.pure-u-md-23-24{width:95.8333%;*width:95.8023%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%;*width:4.1357%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%;*width:8.3023%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%;*width:12.469%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%;*width:16.6357%}.pure-u-lg-1-5{width:20%;*width:19.969%}.pure-u-lg-5-24{width:20.8333%;*width:20.8023%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%;*width:24.969%}.pure-u-lg-7-24{width:29.1667%;*width:29.1357%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%;*width:33.3023%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%;*width:37.469%}.pure-u-lg-2-5{width:40%;*width:39.969%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%;*width:41.6357%}.pure-u-lg-11-24{width:45.8333%;*width:45.8023%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%;*width:49.969%}.pure-u-lg-13-24{width:54.1667%;*width:54.1357%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%;*width:58.3023%}.pure-u-lg-3-5{width:60%;*width:59.969%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%;*width:62.469%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%;*width:66.6357%}.pure-u-lg-17-24{width:70.8333%;*width:70.8023%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%;*width:74.969%}.pure-u-lg-19-24{width:79.1667%;*width:79.1357%}.pure-u-lg-4-5{width:80%;*width:79.969%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%;*width:83.3023%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%;*width:87.469%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%;*width:91.6357%}.pure-u-lg-23-24{width:95.8333%;*width:95.8023%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%;*width:4.1357%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%;*width:8.3023%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%;*width:12.469%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%;*width:16.6357%}.pure-u-xl-1-5{width:20%;*width:19.969%}.pure-u-xl-5-24{width:20.8333%;*width:20.8023%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%;*width:24.969%}.pure-u-xl-7-24{width:29.1667%;*width:29.1357%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%;*width:33.3023%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%;*width:37.469%}.pure-u-xl-2-5{width:40%;*width:39.969%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%;*width:41.6357%}.pure-u-xl-11-24{width:45.8333%;*width:45.8023%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%;*width:49.969%}.pure-u-xl-13-24{width:54.1667%;*width:54.1357%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%;*width:58.3023%}.pure-u-xl-3-5{width:60%;*width:59.969%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%;*width:62.469%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%;*width:66.6357%}.pure-u-xl-17-24{width:70.8333%;*width:70.8023%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%;*width:74.969%}.pure-u-xl-19-24{width:79.1667%;*width:79.1357%}.pure-u-xl-4-5{width:80%;*width:79.969%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%;*width:83.3023%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%;*width:87.469%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%;*width:91.6357%}.pure-u-xl-23-24{width:95.8333%;*width:95.8023%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}} -------------------------------------------------------------------------------- /app/client/pure-0.5.0/purify.js: -------------------------------------------------------------------------------- 1 | var purify = function () { 2 | $('form').addClass('pure-form'); 3 | $('button').addClass('pure-button'); 4 | }; 5 | 6 | Template.atForm.rendered = purify; 7 | -------------------------------------------------------------------------------- /app/client/route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | MeteorSettings.setDefaults({ public: { 6 | blog: { defaultLocale: "en" } 7 | }}); 8 | 9 | Router.configure({ 10 | layoutTemplate: 'mainLayout' 11 | }); 12 | 13 | Router.map(function () { 14 | this.route('home', { 15 | path: '/', 16 | onAfterAction : function() { 17 | document.title = TAPi18n.__('site_title'); 18 | } 19 | }); 20 | }); 21 | 22 | })(); 23 | -------------------------------------------------------------------------------- /app/client/site.js: -------------------------------------------------------------------------------- 1 | Template.header.events({ 2 | 'click .sign-out' : function() { 3 | Meteor.logout(); 4 | } 5 | }); -------------------------------------------------------------------------------- /app/client/site.less: -------------------------------------------------------------------------------- 1 | @import 'constants.import.less'; 2 | 3 | * { 4 | font-family: 'Roboto', sans-serif; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | a, a:visited { 9 | text-decoration: none !important; 10 | color: @xolvio-color; 11 | .transition(.3s) !important; 12 | } 13 | 14 | } 15 | 16 | .fa-github { 17 | font-size: 12em; 18 | color: white; 19 | } 20 | 21 | a:hover, a:focus { 22 | color: @xolvio-orange; 23 | } 24 | 25 | /*Pure override*/ 26 | .pure-menu-heading { 27 | text-transform: none !important; 28 | font-family: 'Roboto Condensed', sans-serif; 29 | } 30 | 31 | a.pure-menu-heading:hover, a.pure-menu-heading:hover a:focus 32 | { 33 | color: @xolvio-color !important; 34 | } 35 | 36 | 37 | .pure-menu-horizontal li a { 38 | // padding: 0; 39 | margin: 0; 40 | border: 0; 41 | // line-height: 0; 42 | } 43 | 44 | /* 45 | * -- BASE STYLES -- 46 | * Most of these are inherited from Base, but I want to change a few. 47 | */ 48 | body { 49 | line-height: 1.7em; 50 | color: #7f8c8d; 51 | font-size: 13px; 52 | } 53 | 54 | h1, 55 | h2, 56 | h3, 57 | h4, 58 | h5, 59 | h6, 60 | label { 61 | color: #34495e; 62 | } 63 | 64 | .pure-img-responsive { 65 | max-width: 100%; 66 | height: auto; 67 | } 68 | 69 | /* 70 | * -- LAYOUT STYLES -- 71 | * These are some useful classes which I will need 72 | */ 73 | .l-box { 74 | padding: 1em; 75 | } 76 | 77 | .l-box-lrg { 78 | padding: 2em; 79 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 80 | } 81 | 82 | .is-center { 83 | text-align: center; 84 | } 85 | 86 | /* 87 | * -- PURE FORM STYLES -- 88 | * Style the form inputs and labels 89 | */ 90 | .pure-form label { 91 | margin: 1em 0 0; 92 | font-weight: bold; 93 | font-size: 100%; 94 | } 95 | 96 | .pure-form input[type] { 97 | border: 2px solid #ddd; 98 | box-shadow: none; 99 | font-size: 100%; 100 | width: 100%; 101 | margin-bottom: 1em; 102 | } 103 | 104 | .pure-form input:focus { 105 | outline-color: @xolvio-orange; 106 | } 107 | 108 | /* 109 | * -- PURE BUTTON STYLES -- 110 | * I want my pure-button elements to look a little different 111 | */ 112 | .pure-button { 113 | background-color: #333; 114 | color: white; 115 | padding: 0.5em 2em; 116 | border-radius: 5px; 117 | } 118 | 119 | a.pure-button-primary:hover { 120 | background: @xolvio-grey-01; 121 | color: @xolvio-orange; 122 | } 123 | a.pure-button-primary { 124 | background-color: @xolvio-orange; 125 | color: @xolvio-white; 126 | border-radius: 5px; 127 | font-size: 150%; 128 | } 129 | 130 | /* 131 | * -- MENU STYLES -- 132 | * I want to customize how my .pure-menu looks at the top of the page 133 | */ 134 | 135 | .home-menu { 136 | padding: 0.5em; 137 | text-align: center; 138 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.10); 139 | } 140 | 141 | .home-menu.pure-menu-open { 142 | background: #333; 143 | } 144 | 145 | .pure-menu.pure-menu-open.pure-menu-fixed { 146 | /* Fixed menus normally have a border at the bottom. */ 147 | border-bottom: none; 148 | /* I need a higher z-index here because of the scroll-over effect. */ 149 | z-index: 4; 150 | } 151 | 152 | .home-menu .pure-menu-heading { 153 | color: white; 154 | font-weight: 400; 155 | font-size: 120%; 156 | } 157 | 158 | .home-menu .pure-menu-selected a { 159 | color: white; 160 | border: 0; 161 | } 162 | 163 | .home-menu a { 164 | color: @xolvio-white; 165 | } 166 | 167 | .home-menu li a:hover, 168 | .home-menu li a:focus { 169 | background: none; 170 | border: none; 171 | color: @xolvio-orange; 172 | } 173 | 174 | /* 175 | * -- SPLASH STYLES -- 176 | * This is the blue top section that appears on the page. 177 | */ 178 | 179 | .splash-container { 180 | background: @xolvio-color; 181 | z-index: 1; 182 | overflow: hidden; 183 | /* The following styles are required for the "scroll-over" effect */ 184 | width: 100%; 185 | // height: 70%; 186 | top: 0; 187 | left: 0; 188 | // position: fixed !important; 189 | } 190 | 191 | .splash { 192 | /* absolute center .splash within .splash-container */ 193 | width: 80%; 194 | // height: 70%; 195 | margin: 7em auto 2em; 196 | // position: absolute; 197 | top: 00px; 198 | left: 0; 199 | bottom: 0; 200 | right: 0; 201 | text-align: center; 202 | /*text-transform: uppercase;*/ 203 | } 204 | 205 | /* This is the main heading that appears on the blue section */ 206 | .splash-head { 207 | a { 208 | color: white; 209 | } 210 | a:hover { 211 | color: @xolvio-orange; 212 | } 213 | font-size: 20px; 214 | font-weight: bold; 215 | color: white; 216 | border: 3px solid white; 217 | padding: 1em 1.6em; 218 | font-weight: 100; 219 | border-radius: 5px; 220 | line-height: 1em; 221 | } 222 | 223 | /* This is the subheading that appears on the blue section */ 224 | .splash-subhead { 225 | color: white; 226 | letter-spacing: 0.05em; 227 | opacity: 0.8; 228 | } 229 | 230 | /* 231 | * -- CONTENT STYLES -- 232 | * This represents the content area (everything below the blue section) 233 | */ 234 | .content-wrapper { 235 | /* These styles are required for the "scroll-over" effect */ 236 | // position: absolute; 237 | // top: 30%; 238 | width: 100%; 239 | min-height: 12%; 240 | z-index: 2; 241 | background: white; 242 | 243 | } 244 | 245 | /* This is the class used for the main content headers (

) */ 246 | .content-head { 247 | font-weight: 400; 248 | /*text-transform: uppercase;*/ 249 | letter-spacing: 0.1em; 250 | margin: 2em 0 1em; 251 | } 252 | 253 | /* This is a modifier class used when the content-head is inside a ribbon */ 254 | .content-head-ribbon { 255 | color: white; 256 | } 257 | 258 | /* This is the class used for the content sub-headers (

) */ 259 | .content-subhead { 260 | color: @xolvio-color; 261 | } 262 | 263 | .content-subhead i { 264 | margin-right: 7px; 265 | } 266 | 267 | /* This is the class used for the dark-background areas. */ 268 | .ribbon { 269 | background: #333; 270 | color: #aaa; 271 | } 272 | 273 | /* This is the class used for the footer */ 274 | .footer { 275 | background: #111; 276 | } 277 | 278 | /* 279 | * -- TABLET (AND UP) MEDIA QUERIES -- 280 | * On tablets and other medium-sized devices, we want to customize some 281 | * of the mobile styles. 282 | */ 283 | @media (min-width: 48em) { 284 | 285 | /* We increase the body font size */ 286 | body { 287 | font-size: 16px; 288 | } 289 | 290 | /* We want to give the content area some more padding */ 291 | .content { 292 | padding: 1em; 293 | } 294 | 295 | /* We can align the menu header to the left, but float the 296 | menu items to the right. */ 297 | .home-menu { 298 | text-align: left; 299 | } 300 | 301 | .home-menu ul { 302 | float: right; 303 | } 304 | 305 | /* We increase the height of the splash-container */ 306 | /* .splash-container { 307 | height: 500px; 308 | }*/ 309 | /* We decrease the width of the .splash, since we have more width 310 | to work with */ 311 | .splash { 312 | width: 50%; 313 | height: 50%; 314 | } 315 | 316 | .splash-head { 317 | font-size: 250%; 318 | } 319 | 320 | /* We remove the border-separator assigned to .l-box-lrg */ 321 | .l-box-lrg { 322 | border: none; 323 | } 324 | 325 | } 326 | 327 | /* 328 | * -- DESKTOP (AND UP) MEDIA QUERIES -- 329 | * On desktops and other large devices, we want to over-ride some 330 | * of the mobile and tablet styles. 331 | */ 332 | @media (min-width: 78em) { 333 | /* We increase the header font size even more */ 334 | .splash-head { 335 | font-size: 300%; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /app/client/startup.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | 3 | Tracker.autorun(function() { 4 | 5 | T9n.setLanguage(Session.get('locale')); 6 | }); 7 | 8 | Session.set('locale', Meteor.settings.public.blog.defaultLocale); 9 | }); 10 | -------------------------------------------------------------------------------- /app/gs-cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "origin": ["*"], 4 | "responseHeader": ["Content-Type"], 5 | "method": ["GET", "POST", "PUT", "HEAD"], 6 | "maxAgeSeconds": 3000 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /app/i18n/en.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Xolv.io MD-Blog", 3 | "description": "A WYSIWYG Markdown blog for your Meteor app.", 4 | "site_meta_desc": "An example page showing you how to use Xolv.io's md-blog.", 5 | "site_title": "Xolv.io Markdown Blog Sample Site", 6 | "home": "Home", 7 | "blog": "Blog", 8 | "sign_out": "Sign out", 9 | "register": "Register", 10 | "sign_in": "Sign in", 11 | "md_blog_desc": "A drop-in markdown blog for your Meteor app.", 12 | "see_a_demo": "See a Demo", 13 | "home_title": "meteor add xolvio:md-blog", 14 | "feature1_title": "WYSIWYG Markdown", 15 | "feature1_desc": "Write your blog posts in the popular markdown syntax on the left, and see your changes live on the right.", 16 | "feature2_title": "Simple Workflow", 17 | "feature2_desc": "You can create, publish, unpublish, archive, unarchive and delete blog posts.", 18 | "feature3_title": "Syntax Highlighting", 19 | "feature3_desc": "Uses %s which contains lots of themes to choose from to suit the look and feel of your site.", 20 | "feature4_title": "Customizable", 21 | "feature4_desc": "This demo site uses the %s responsive framework, but you can use anything you like from Bootstrap to Foundation.", 22 | "github_link": "Fork/Star on GitHub", 23 | "love_open_source": "We __heart__ the open-source community and welcome feedback and contributions.", 24 | "or": "or", 25 | "to_author_posts": " to author posts" 26 | } 27 | -------------------------------------------------------------------------------- /app/i18n/fr.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MD-Blog de Xolv.io", 3 | "description": "Un blog markdown prêt-à-utiliser pour votre application Meteor.", 4 | "site_meta_desc": "Un example de site démontrant l'utilisation du package md-blog de Xolv.io.", 5 | "site_title": "Site example du blog Markdown de Xolv.io", 6 | "home": "Site", 7 | "blog": "Blog", 8 | "sign_out": "Fermer la session", 9 | "register": "S'enregistrer", 10 | "sign_in": "Ouvrir une session", 11 | "md_blog_desc": "Un blog markdown prêt-à-utiliser pour votre application Meteor.", 12 | "see_a_demo": "Voir une démo", 13 | "home_title": "meteor add xolvio:md-blog", 14 | "feature1_title": "Éditeur Markdown intuitif", 15 | "feature1_desc": "Écrivez vos articles avec la syntaxe populaire 'Markdown' dans une zone à gauche, et voyez le rendu immédiatement à droite.", 16 | "feature2_title": "Workflow simple", 17 | "feature2_desc": "Vous pouvez créer, publier, retirer, archiver, remettre et supprimer les articles.", 18 | "feature3_title": "Code en couleur", 19 | "feature3_desc": "Utilise %s, qui contient un grand nombre de thèmes pour convenir au rendu de votre site.", 20 | "feature4_title": "Personnalisation", 21 | "feature4_desc": "Ce site de démo utilise le framework %s, mais vous pouvez utiliser ce que vous désirez, comme Bootstrap ou encore Foundation.", 22 | "github_link": "Fork/Star sur GitHub", 23 | "love_open_source": "Nous __heart__ la communauté du logiciel libre et accueuillons vos opinions et contributions.", 24 | "or": "ou", 25 | "to_author_posts": " pour écrire des articles" 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/slingshot-restrictions.js: -------------------------------------------------------------------------------- 1 | Slingshot.fileRestrictions("mdblog-pictures", { 2 | allowedFileTypes: ["image/png", "image/jpeg", "image/gif"], 3 | maxSize: 10 * 1024 * 1024 // 10 MB (use null for unlimited) 4 | }); 5 | -------------------------------------------------------------------------------- /app/packages/src: -------------------------------------------------------------------------------- 1 | ../../src -------------------------------------------------------------------------------- /app/private/.gitignore: -------------------------------------------------------------------------------- 1 | google-cloud-service-key.pem 2 | google-cloud-access-id.txt 3 | -------------------------------------------------------------------------------- /app/private/publish_email_en.html: -------------------------------------------------------------------------------- 1 |

{{summary}}

2 | Read more → 3 | 4 |

This email was sent with the xolvio:md-blog Meteor package

5 | -------------------------------------------------------------------------------- /app/private/publish_email_fr.html: -------------------------------------------------------------------------------- 1 |

{{summary}}

2 | Lisez la suite → 3 | 4 |

Ce courriel a été envoyé avec le package Meteor xolvio:md-blog

5 | -------------------------------------------------------------------------------- /app/project-tap.i18n: -------------------------------------------------------------------------------- 1 | { 2 | "helper_name": "_", 3 | "supported_languages": null, 4 | "i18n_files_route": "/tap-i18n", 5 | "cdn_path": null 6 | } 7 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolvio/md-blog/e769d23e1c08c826245fffc4c025161d030a97d8/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/img/common/file-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolvio/md-blog/e769d23e1c08c826245fffc4c025161d030a97d8/app/public/img/common/file-icons.png -------------------------------------------------------------------------------- /app/public/img/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolvio/md-blog/e769d23e1c08c826245fffc4c025161d030a97d8/app/public/img/markdown.png -------------------------------------------------------------------------------- /app/server/emails.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | 3 | // compile the email templates for the blog's locale 4 | var locale = Meteor.settings.public.blog.defaultLocale; 5 | SSR.compileTemplate('publishEmail', 6 | Assets.getText('publish_email_' + locale + '.html')); 7 | }); 8 | -------------------------------------------------------------------------------- /app/server/slingshot.js: -------------------------------------------------------------------------------- 1 | MeteorSettings.setDefaults({ public: 2 | { blog: 3 | { pictures: 4 | { Slingshot: { 5 | bucket: 'mdblog-app-files', 6 | directive: 'mdblog-pictures', 7 | pemFile: 'google-cloud-service-key.pem', 8 | idTextFile: 'google-cloud-access-id.txt' 9 | } 10 | } 11 | } 12 | } 13 | }); 14 | 15 | Meteor.startup(function () { 16 | 17 | var settings = Meteor.settings.public.blog.pictures.Slingshot; 18 | 19 | var pemText = (settings.pemFile) ? Assets.getText(settings.pemFile).trim() : null; 20 | var idText = (settings.idTextFile) ? Assets.getText(settings.idTextFile).trim() : null; 21 | 22 | if (idText && pemText) { 23 | console.info("Initializing Slingshot for Google Cloud Storage."); 24 | 25 | Slingshot.GoogleCloud.directiveDefault.GoogleSecretKey = pemText; 26 | Slingshot.GoogleCloud.directiveDefault.GoogleAccessId = idText; 27 | 28 | Slingshot.createDirective(settings.directive, Slingshot.GoogleCloud, { 29 | bucket: settings.bucket, 30 | acl: "public-read", 31 | 32 | authorize: function () { 33 | /* Deny uploads if user is not logged in */ 34 | if (!this.userId) { 35 | var message = "Please login before posting files"; 36 | throw new Meteor.Error("Login Required", message); 37 | } 38 | 39 | return true; 40 | }, 41 | 42 | key: function (file) { 43 | /* TODO use a directory */ 44 | return file.name; 45 | } 46 | }); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /app/server/users.js: -------------------------------------------------------------------------------- 1 | Accounts.onCreateUser(function (options, user) { 2 | user.roles = ['mdblog-author']; 3 | if (options.profile) 4 | user.profile = options.profile; 5 | return user; 6 | }); 7 | -------------------------------------------------------------------------------- /app/settings-meteor-com.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": { 3 | "blog": { 4 | "prettify": { 5 | "syntax-highlighting": true, 6 | "element-classes": [ 7 | { 8 | "locator": "img", 9 | "classes": ["pure-img"] 10 | }, 11 | { 12 | "locator": "button", 13 | "classes": ["pure-button"] 14 | } 15 | ] 16 | }, 17 | "sortBy": {"date": -1}, 18 | "baseUrl": "http://md-blog.meteor.com", 19 | "blogPath": "/blog", 20 | "archivePath": "/blog/archive", 21 | "useUniqueBlogPostsPath": true, 22 | "defaultLocale": "en" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": { 3 | "blog": { 4 | "prettify": { 5 | "syntax-highlighting": true, 6 | "element-classes": [ 7 | { 8 | "locator": "img", 9 | "classes": ["pure-img"] 10 | }, 11 | { 12 | "locator": "button", 13 | "classes": ["pure-button"] 14 | } 15 | ] 16 | }, 17 | "sortBy": {"date": -1}, 18 | "baseUrl": "http://localhost:3000", 19 | "blogPath": "/blog", 20 | "archivePath": "/blog/archive", 21 | "useUniqueBlogPostsPath": true, 22 | "defaultLocale": "en", 23 | "emailOnPublish": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .versions 3 | -------------------------------------------------------------------------------- /src/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /src/client/blog-client.js: -------------------------------------------------------------------------------- 1 | var momentLocaleDep = new Tracker.Dependency(); 2 | 3 | Meteor.startup(function() { 4 | 5 | var locale = Meteor.settings.public.blog.defaultLocale; 6 | Session.set('locale', locale); 7 | 8 | Tracker.autorun(function() { 9 | var locale = Session.get('locale'); 10 | TAPi18n.setLanguage(locale) 11 | .fail(function (error_message) { 12 | console.log('ERROR setting language: ' + error_message); 13 | }) 14 | .done(function() { 15 | var momentConfig = $.parseJSON(TAPi18n.__("moment")); 16 | moment.locale(locale, momentConfig); 17 | momentLocaleDep.changed(); 18 | }); 19 | }); 20 | }); 21 | 22 | // *********************************************************************************************** 23 | // **** Blog List 24 | 25 | Template.blogList.rendered = function () { 26 | Tracker.autorun(function() { 27 | Session.get('locale'); // force dependency 28 | _setMetadata({ 29 | title: TAPi18n.__("name"), 30 | description: TAPi18n.__("description") 31 | }); 32 | }); 33 | }; 34 | 35 | var _setMetadata = function (meta) { 36 | if (meta.title) { 37 | document.title = meta.title; 38 | delete meta.title; 39 | } 40 | for (var key in meta) { 41 | $('meta[name="' + key + '"]').remove(); 42 | $('head').append(''); 43 | 44 | $('meta[property="og:' + key + '"]').remove(); 45 | $('head').append(''); 46 | } 47 | }; 48 | 49 | Template.blogList.created = function () { 50 | var self = this; 51 | self.serverBlogCount = new ReactiveVar(false); 52 | this.autorun(function () { 53 | Meteor.call('mdBlogCount', function (err, serverBlogCount) { 54 | self.serverBlogCount.set(serverBlogCount); 55 | }); 56 | }); 57 | }; 58 | 59 | Template.blogList.helpers({ 60 | content: function () { 61 | return marked(this.summary); 62 | } 63 | }); 64 | 65 | Template.blogList.events({ 66 | 'click #mdblog-new': _new 67 | }); 68 | 69 | function _new () { 70 | if (!Meteor.user()) { 71 | this.render('not-found'); 72 | return; 73 | } 74 | 75 | var author = Meteor.user().profile && Meteor.user().profile.name ? Meteor.user().profile.name : Meteor.user().emails[0].address; 76 | var newBlog = { 77 | title: TAPi18n.__("new_post_title"), 78 | date: new Date(), 79 | author: author, 80 | summary: _getRandomSummary(), 81 | content: TAPi18n.__("new_post_contents") 82 | }; 83 | Meteor.call('upsertBlog', newBlog, function(err, blog) { 84 | if (!err) { 85 | Router.go('blogPost', blog); 86 | } else { 87 | console.log('Error upserting blog', err); 88 | } 89 | 90 | }); 91 | 92 | } 93 | 94 | function _getRandomSummary () { 95 | var subjects = ['I', 'You', 'She', 'He', 'They', 'We']; 96 | var verbs = ['was just', 'will get', 'found', 'attained', 'received', 'will merge with', 'accept', 'accepted']; 97 | var objects = ['Billy', 'an apple', 'a force', 'the treasure', 'a sheet of paper']; 98 | var endings = ['.', ', right?', '.', ', like I said I would.', '.', ', just like your app!']; 99 | 100 | return subjects[Math.round(Math.random() * ( 101 | subjects.length - 1))] + ' ' + 102 | verbs[Math.round(Math.random() * (verbs.length - 1))] + ' ' + 103 | objects[Math.round(Math.random() * (objects.length - 1))] + 104 | endings[Math.round(Math.random() * (endings.length - 1))]; 105 | } 106 | 107 | // *********************************************************************************************** 108 | // **** Blog Post 109 | 110 | Template.blogPost.rendered = function () { 111 | if (this.data) { 112 | _setMetadata({ 113 | title: this.data.title, 114 | description: this.data.summary 115 | }); 116 | } 117 | }; 118 | 119 | Template.blogPost.events({ 120 | 'click [contenteditable], focus *[contenteditable]': _edit, 121 | 'keyup [contenteditable]': _update, 122 | 'blur [contenteditable]': _stopEditing, 123 | 'dropped #content': _droppedPicture, 124 | 'click #mdblog-picture': _choosePicture, 125 | 'change #mdblog-file-input': _inputPicture 126 | }); 127 | 128 | Template.blogPost.helpers({ 129 | blogPostReady: function () { 130 | return this.content; 131 | }, 132 | content: function () { 133 | var $content = $('

', {html: marked(this.content)}); 134 | _prettify($content); 135 | return $content.html(); 136 | }, 137 | contenteditable: function () { 138 | if (UI._globalHelpers.isInRole('mdblog-author')) { 139 | return 'contenteditable'; 140 | } 141 | return ''; 142 | }, 143 | allowPictureUpload: function () { 144 | return UI._globalHelpers.isInRole('mdblog-author') && _allowPictureUpload(); 145 | } 146 | }); 147 | 148 | function _update (ev) { 149 | if (ev.keyCode == 27) { 150 | $(element).blur(); 151 | return; 152 | } 153 | var element = ev.currentTarget; 154 | __update(this, element); 155 | } 156 | 157 | __update = function (blogPost, element) { 158 | Session.set('mdblog-modified', true); 159 | $('#mdblog-publish').show(); 160 | blogPost[element.id] = element.innerText; 161 | if ($(element).data('markdown')) { 162 | var $content = $('

', {html: marked(element.innerText)}); 163 | _prettify($content); 164 | $('#mdblog-clone')[0].innerHTML = $content.html(); 165 | } 166 | }; 167 | 168 | function _edit (ev) { 169 | var $el = $(ev.currentTarget); 170 | if ($el.data('editing')) { 171 | return; 172 | } 173 | if ($el.data('markdown')) { 174 | var clone = $el.clone(); 175 | $el.after(clone.attr('id', 'mdblog-clone')); 176 | } 177 | $el.addClass('editing'); 178 | $el.data('editing', true); 179 | ev.currentTarget.innerText = this[ev.currentTarget.id]; 180 | } 181 | 182 | function _stopEditing (ev) { 183 | var element = ev.currentTarget; 184 | var $el = $(element); 185 | $el.data('editing', false); 186 | $el.removeClass('editing'); 187 | 188 | this[element.id] = element.innerText; 189 | if ($el.data('markdown')) { 190 | $('#mdblog-clone').remove(); 191 | var $content = $('

', {html: marked(element.innerText)}); 192 | _prettify($content); 193 | element.innerHTML = $content.html(); 194 | } 195 | // TODO save changes to this.draft.history.time.[field] 196 | } 197 | 198 | function _addSyntaxHighlighting (e) { 199 | var syntaxHighlighting = Meteor.settings.public.blog.prettify['syntax-highlighting']; 200 | if (syntaxHighlighting) { 201 | e.find('pre').each(function (i, block) { 202 | hljs.highlightBlock(block); 203 | }); 204 | } 205 | } 206 | 207 | function _addClassesToElements (e) { 208 | var elementClasses = Meteor.settings.public.blog.prettify['element-classes']; 209 | if (elementClasses) { 210 | var addClass = function (idx, element) { 211 | var $elem = $(element); 212 | $elem.addClass(elementClasses[i].classes.join(' ')); 213 | }; 214 | for (var i = 0; i < elementClasses.length; i++) { 215 | e.find(elementClasses[i].locator).each(addClass); 216 | } 217 | } 218 | } 219 | 220 | function _prettify (content) { 221 | if (Meteor.settings.public.blog.prettify) { 222 | _addSyntaxHighlighting(content); 223 | _addClassesToElements(content); 224 | } 225 | } 226 | 227 | // **** File Upload 228 | 229 | function _droppedPicture(event, template) { 230 | if (_allowPictureUpload()) { 231 | _insertPictures (template.data, event.originalEvent.dataTransfer.files); 232 | } 233 | } 234 | 235 | function _choosePicture () { 236 | $('#mdblog-file-input').click(); 237 | } 238 | function _inputPicture (event, template) { 239 | _insertPictures (template.data, event.target.files); 240 | } 241 | 242 | // *********************************************************************************************** 243 | // **** Blog Controls 244 | 245 | Router.onAfterAction(function () { 246 | Session.set('mdblog-modified', false); 247 | }); 248 | 249 | Template.blogControls.helpers({ 250 | modified: function () { 251 | return Session.get('mdblog-modified'); 252 | } 253 | }); 254 | 255 | Template.blogControls.events({ 256 | 'click #mdblog-save': _save, 257 | 'click #mdblog-publish': _publish, 258 | 'click #mdblog-unpublish': _unpublish, 259 | 'click #mdblog-archive': _archive, 260 | 'click #mdblog-unarchive': _unarchive, 261 | 'click #mdblog-delete': _delete 262 | }); 263 | 264 | function _save () { 265 | if (this.published) { 266 | var userIsSure = confirm(TAPi18n.__("confirm_save_published")); 267 | if (!userIsSure) { 268 | return; 269 | } 270 | } 271 | Meteor.call('upsertBlog', this, function (err, blog) { 272 | if (!err) { 273 | Router.go('blogPost', blog); 274 | Session.set('mdblog-modified', false); 275 | } 276 | }); 277 | } 278 | 279 | function _publish () { 280 | var userIsSure = confirm(TAPi18n.__("confirm_publish")); 281 | if (userIsSure) { 282 | this.published = true; 283 | Meteor.call('upsertBlog', this); 284 | if (Meteor.settings.public.blog.emailOnPublish) { 285 | Meteor.call('sendEmail', this); 286 | } 287 | } 288 | } 289 | 290 | function _unpublish () { 291 | var userIsSure = confirm(TAPi18n.__("confirm_unpublish")); 292 | if (userIsSure) { 293 | this.published = false; 294 | Meteor.call('upsertBlog', this); 295 | } 296 | } 297 | 298 | function _archive () { 299 | var userIsSure = confirm(TAPi18n.__("confirm_archive")); 300 | if (userIsSure) { 301 | this.archived = true; 302 | Meteor.call('upsertBlog', this); 303 | } 304 | } 305 | 306 | function _unarchive () { 307 | var userIsSure = confirm(TAPi18n.__("confirm_unarchive")); 308 | if (userIsSure) { 309 | this.archived = false; 310 | Meteor.call('upsertBlog', this); 311 | } 312 | } 313 | 314 | function _delete () { 315 | var input = prompt(TAPi18n.__("confirm_delete")); 316 | if (input === TAPi18n.__("confirm_delete_YES")) { 317 | Meteor.call('deleteBlog', this, function (e) { 318 | if (!e) { 319 | Router.go('blogList'); 320 | } 321 | }); 322 | } 323 | } 324 | 325 | 326 | UI.registerHelper('mdBlogDate', function (date) { 327 | momentLocaleDep.depend(); 328 | return moment(date).calendar(); 329 | }); 330 | 331 | UI.registerHelper('mdBlogElementClasses', function (type) { 332 | var elementClasses = Meteor.settings.public.blog.prettify['element-classes']; 333 | if (elementClasses) { 334 | for (var i = 0; i < elementClasses.length; i++) { 335 | if (elementClasses[i].locator === type) { 336 | return elementClasses[i].classes.join(' '); 337 | } 338 | } 339 | } 340 | 341 | }); 342 | -------------------------------------------------------------------------------- /src/client/blog-picture.js: -------------------------------------------------------------------------------- 1 | MeteorSettings.setDefaults({ public: 2 | { blog: 3 | { pictures: { 4 | maxWidth : 800, 5 | maxHeight : 800 6 | } 7 | } 8 | } 9 | }); 10 | 11 | _allowPictureUpload = function () { 12 | 13 | return Meteor.settings.public.blog.pictures.Slingshot; 14 | }; 15 | 16 | _insertPictures = function (blogPost, files) { 17 | 18 | _.each(files, function (file) { 19 | /* Filter out dropped non-image files */ 20 | if (file.type.indexOf("image") === 0) { 21 | __insertPicture (blogPost, file); 22 | } 23 | }); 24 | }; 25 | 26 | function __insertPicture (blogPost, file) { 27 | 28 | var maxWidth = Meteor.settings.public.blog.pictures.maxWidth; 29 | var maxHeight = Meteor.settings.public.blog.pictures.maxHeight; 30 | 31 | function doneUploading (error, downloadUrl) { 32 | 33 | console.info('Done uploading:', downloadUrl, 'Error:', error); 34 | if (error) { 35 | alert (error); 36 | } 37 | else { 38 | var element = $('article#content')[0]; 39 | element.focus(); 40 | var markdown = "![" + file.name + "](" + downloadUrl + ")
"; 41 | element.innerHTML = markdown + element.innerHTML; 42 | __update(blogPost, element); 43 | } 44 | } 45 | 46 | function uploadAndInsert (dataURI) { 47 | 48 | var data = dataURI.substring(dataURI.indexOf(',') + 1); 49 | var blob = b64toBlob(data, file.type); 50 | blob.name = file.name; 51 | if (file.size < blob.size) { 52 | /* upload the smallest of the two files */ 53 | console.warn('Warning: ignored processing as it resulted in larger file.', file.size, blob.size); 54 | blob = file; 55 | } 56 | 57 | var uploader = new Slingshot.Upload(Meteor.settings.public.blog.pictures.Slingshot.directive); 58 | uploader.send( blob, doneUploading ); 59 | } 60 | 61 | processImage( file, maxWidth, maxHeight, uploadAndInsert ); 62 | } 63 | 64 | /* From http://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript 65 | * TODO use https://github.com/eligrey/canvas-toBlob.js 66 | */ 67 | function b64toBlob(b64Data, contentType, sliceSize) { 68 | contentType = contentType || ''; 69 | sliceSize = sliceSize || 512; 70 | var byteCharacters = atob(b64Data); 71 | var byteArrays = []; 72 | for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { 73 | var slice = byteCharacters.slice(offset, offset + sliceSize); 74 | var byteNumbers = new Array(slice.length); 75 | for (var i = 0; i < slice.length; i++) { 76 | byteNumbers[i] = slice.charCodeAt(i); 77 | } 78 | var byteArray = new Uint8Array(byteNumbers); 79 | byteArrays.push(byteArray); 80 | } 81 | return new Blob( byteArrays, { type: contentType } ); 82 | } 83 | -------------------------------------------------------------------------------- /src/client/blog-route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | MeteorSettings.setDefaults({ public: { blog: {} }}); 6 | 7 | var blogSub = Meteor.subscribe('blog'); 8 | 9 | Router.map(function () { 10 | this.route('blogList', { 11 | path: getBaseBlogPath(), 12 | layoutTemplate: 'blogListLayout', 13 | action: function () { 14 | this.wait(blogSub); 15 | this.render('blogList'); 16 | }, 17 | data: function () { 18 | var sort = Meteor.settings.public.blog.sortBy; 19 | return Blog.find({archived: false}, {sort: sort ? sort : {date: -1}}); 20 | } 21 | }); 22 | 23 | this.route('blogListArchive', { 24 | path: getBaseArchivePath(), 25 | layoutTemplate: 'blogListLayout', 26 | action: function () { 27 | this.wait(blogSub); 28 | this.render('blogList'); 29 | }, 30 | data: function () { 31 | var sort = Meteor.settings.public.blog.sortBy; 32 | return Blog.find({archived: true}, {sort: sort ? sort : {date: -1}}); 33 | } 34 | }); 35 | 36 | this.route('blogPost', { 37 | path: getBlogPostPath(':shortId', ':slug'), 38 | layoutTemplate: 'blogPostLayout', 39 | action: function () { 40 | this.wait(blogSub); 41 | this.render('blogPost'); 42 | }, 43 | data: function () { 44 | if (this.ready()) { 45 | var blog = Blog.findOne({slug: this.params.slug}); 46 | if (blog) { 47 | blog.loaded = true; 48 | return blog; 49 | } 50 | this.render('not-found') 51 | } 52 | } 53 | }); 54 | 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /src/client/blog-templates.html: -------------------------------------------------------------------------------- 1 | 30 | 31 | 58 | 59 | 89 | -------------------------------------------------------------------------------- /src/client/blog.less: -------------------------------------------------------------------------------- 1 | section.blog-controls { 2 | 3 | } 4 | 5 | .blog-list { 6 | margin: 20px; 7 | .blog-post { 8 | margin: 50px 0; 9 | border-bottom: 2px solid rgba(0,0,0,0.2); 10 | 11 | header { 12 | h1 { 13 | margin: 0; 14 | } 15 | .info { 16 | border-bottom: 1px dashed rgba(0,0,0,0.2); 17 | opacity: .8; 18 | .author { 19 | } 20 | time { 21 | } 22 | } 23 | } 24 | summary { 25 | font-size: medium; 26 | margin: 20px 0; 27 | } 28 | } 29 | } 30 | 31 | .blog-post { 32 | header { 33 | h1 { 34 | } 35 | .info { 36 | color: #666; 37 | 38 | .author { 39 | font-weight: bold; 40 | color: orange; 41 | } 42 | time { 43 | font-style: italic; 44 | } 45 | } 46 | } 47 | article { 48 | &.editing { 49 | float: left; 50 | background-color: rgba(0,0,0,.1); 51 | width: 49%; 52 | margin-top: 1em; 53 | } 54 | } 55 | #mdblog-clone { 56 | float: right; 57 | width: 49%; 58 | } 59 | } 60 | 61 | #mdblog-new { 62 | background-color: rgba(0,0,0,1); 63 | } 64 | 65 | #mdblog-save { 66 | background-color: rgba(50,50,200,1); 67 | } 68 | 69 | #mdblog-unpublish { 70 | background-color: rgba(200,0,0,1); 71 | } 72 | 73 | #mdblog-publish { 74 | background-color: rgba(0,156,0,1); 75 | } 76 | 77 | #mdblog-archive { 78 | background-color: rgba(0,0,0,.5); 79 | } 80 | 81 | #mdblog-unarchive { 82 | background-color: rgba(0,0,0,.5); 83 | } 84 | 85 | #mdblog-delete { 86 | background-color: rgba(200,100,100,1); 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/common/blog-collections.js: -------------------------------------------------------------------------------- 1 | Blog = new Mongo.Collection('blog'); -------------------------------------------------------------------------------- /src/common/blog-paths.js: -------------------------------------------------------------------------------- 1 | 2 | getBaseBlogPath = function () { 3 | return Meteor.settings.public.blog.blogPath; 4 | } 5 | 6 | getBaseArchivePath = function () { 7 | return Meteor.settings.public.blog.archivePath; 8 | } 9 | 10 | getBlogPostPath = function (shortId, slug) { 11 | 12 | var path = getBaseBlogPath(); 13 | if (Meteor.settings.public.blog.useUniqueBlogPostsPath) { 14 | path += '/' + shortId; 15 | } 16 | path += '/' + slug; 17 | 18 | return sanitizePath(path); 19 | } 20 | 21 | getBlogPostUrl = function (post) { 22 | 23 | return Meteor.settings.public.blog.baseUrl + '/' 24 | + getBlogPostPath(post.shortId, post.slug); 25 | } 26 | 27 | /* 28 | * Remove any duplicate '/' 29 | */ 30 | sanitizePath = function (path) { 31 | return _.compact(path.split('/')).join('/'); 32 | } 33 | -------------------------------------------------------------------------------- /src/doc/dragdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolvio/md-blog/e769d23e1c08c826245fffc4c025161d030a97d8/src/doc/dragdrop.png -------------------------------------------------------------------------------- /src/i18n/en.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "new": "New", 3 | "posted_by": "Posted by ", 4 | "read_more": "Read more", 5 | "waiting_for_posts": "Waiting for blog posts.", 6 | "save": " Save", 7 | "publish": " Publish", 8 | "unpublish": " Unpublish", 9 | "archive": " Archive", 10 | "unarchive": " Unarchive", 11 | "picture": "Insert Picture", 12 | "delete": " Delete", 13 | "loading": "Loading...", 14 | 15 | "confirm_save_published": "This blog entry is already published. Saved changes will be immediately visible to users and search engines.\nClick OK if you are sure.", 16 | "confirm_publish": "Blog entry will be visible to users or search engines.\nClick OK if you are sure.", 17 | "confirm_unpublish": "Blog entry will no longer be visible to users or search engines.\nClick OK if you are sure.", 18 | "confirm_archive": "Archiving this entry will remove it from the main list view.\nClick OK if you are sure.", 19 | "confirm_unarchive": "Unarchiving this entry will put it back into the main list view.\nClick OK if you are sure.", 20 | "confirm_delete": "Please type YES in capitals if you are sure you want to delete this entry.\nThis action is non-reversible, you should consider archiving instead.", 21 | "confirm_delete_YES": "YES", 22 | 23 | "blog_post_setup_title": "This Blog is not Setup Yet", 24 | "blog_post_setup_author": "John Doe", 25 | "blog_post_setup_summary": "If you are the owner of this blog, you need to setup a couple of things before the blog will magically work.", 26 | "blog_post_setup_contents": [ 27 | "Welcome to your blog!", 28 | "===", 29 | "[Click here to see the documentation](https://github.com/xolvio/md-blog)" 30 | ], 31 | 32 | "new_post_title": "Click Anywhere to Edit", 33 | "new_post_contents": [ 34 | "##**Main Content**", 35 | "This is a content editable field. Click here to start editing. Full markdown is supported in here.", 36 | "\n##**Extra Formatting Options**", 37 | "In addition to the markdown support, you also get the following features:", 38 | "\n###**Pretty Code**", 39 | "\n```javascript", 40 | "// check out my method", 41 | "function myMethod() {", 42 | " return new Object();", 43 | "};", 44 | "```\n", 45 | "\n###**HTML Tags**", 46 | "Non-markdown markup is also supported, like underline super and sub script", 47 | "\n", 48 | "\n###**Highly Customizable**", 49 | "Custom class mappings for prettifying elements allows you to customize the ", 50 | "markdown further. For example, you can add classes to make images responsive using ", 51 | "the UI framework of your choice.", 52 | "![alt text](http://www.meteortesting.com/img/og.png \"Image Text\")" 53 | ], 54 | 55 | "moment": [ 56 | "{", 57 | "\"calendar\": {", 58 | "\"lastDay\": \"[yesterday at] LT\",", 59 | "\"sameDay\": \"[today at] LT\",", 60 | "\"nextDay\": \"[tomorrow at] LT\",", 61 | "\"lastWeek\": \"[last] dddd [at] LT\",", 62 | "\"nextWeek\": \"dddd [at] LT\",", 63 | "\"sameElse\": \"on L\"", 64 | "}", 65 | "}" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/i18n/fr.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "new": "Nouveau", 3 | "posted_by": "Écrit par ", 4 | "read_more": "Lire la suite", 5 | "waiting_for_posts": "En attente d'articles ...", 6 | "save": " Sauvegarder", 7 | "publish": " Publier", 8 | "unpublish": " Retirer", 9 | "archive": " Archiver", 10 | "unarchive": " Remettre", 11 | "picture": "Insérer une image", 12 | "delete": " Supprimer", 13 | "loading": "Chargement en cours ...", 14 | 15 | "confirm_save_published": "Cet article est déjà publié. Les modifications seront immédiatement visibles des utilisateurs et engins de recherche.\nCliquez OK si vous êtes certain.", 16 | "confirm_publish": "Cet article sera visible des utilisateurs et engins de recherche.\nCliquez OK si vous êtes certain.", 17 | "confirm_unpublish": "Cet article ne sera plus visible des utilisateurs et engins de recherche.\nCliquez OK si vous êtes certain.", 18 | "confirm_archive": "Archiver cet article l'enlèvera de la liste principale.\nCliquez OK si vous êtes certain.", 19 | "confirm_unarchive": "Remettre cette entrée dans la liste principale ?\nCliquez OK si vous êtes certain.", 20 | "confirm_delete": "Entrez OUI en lettre capitales si vous êtes certain de vouloir supprimer cet article.\nCette action est irréversible ; vous devriez considérer l'archivage à la place.", 21 | "confirm_delete_YES": "OUI", 22 | 23 | "blog_post_setup_title": "Ce blog n'est pas encore configuré", 24 | "blog_post_setup_author": "Charlie", 25 | "blog_post_setup_summary": "Si vous êtes le propriétaire de ce blog, vous devez configurer certaines choses avant qu'il fonctionne par magie.", 26 | "blog_post_setup_contents": [ 27 | "Bienvenue dans votre blog !", 28 | "===", 29 | "Cliquez ici pour [voir la doc](https://github.com/xolvio/md-blog)" 30 | ], 31 | 32 | "new_post_title": "Cliquez n'importe où pour éditer", 33 | "new_post_contents": [ 34 | "##**Contenu principal**", 35 | "Ceci est un champ au contenu modifiable. Cliquer ici pour débuter l'édition. La syntaxe markdown est supportée.", 36 | "\n##**Options de formattage supplémentaires**", 37 | "En supplément du support de markdown, vous bénéficiez également de :", 38 | "\n###**Présentation du code avec couleurs**", 39 | "\n```javascript", 40 | "// check out my method", 41 | "function myMethod() {", 42 | " return new Object();", 43 | "};", 44 | "```\n", 45 | "\n###**Tags HTML**", 46 | "Les tags non-markdown sont également supportés, comme souligné exposant ou indice", 47 | "\n", 48 | "\n###**Hautement configurable**", 49 | "Vous pouvez utiliser des classes CSS personnalisées pour modifier le rendu du markdown.", 50 | "Vous pouvez par exemple ajouter des classes pour rendre les images interactives", 51 | " en utilisant le framework graphique de votre choix.", 52 | "![alt text](http://www.meteortesting.com/img/og.png \"Texte de l'image\")" 53 | ], 54 | 55 | "moment": [ 56 | "{", 57 | "\"weekdays\": [ \"lundi\", \"mardi\", \"mercredi\", \"jeudi\", \"vendredi\", \"samedi\", \"dimanche\" ],", 58 | "\"calendar\": {", 59 | "\"lastDay\": \"[hier à] LT\",", 60 | "\"sameDay\": \"[aujourd'hui à] LT\",", 61 | "\"nextDay\": \"[demain à] LT\",", 62 | "\"lastWeek\": \"dddd [dernier à] LT\",", 63 | "\"nextWeek\": \"dddd [à] LT\",", 64 | "\"sameElse\": \"DD/MM/YYYY\"", 65 | "}", 66 | "}" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/package-tap.i18n: -------------------------------------------------------------------------------- 1 | { 2 | // The name for the translation function that 3 | // will be available in package's namespace. 4 | "translation_function_name": "__", 5 | 6 | // the name for the package templates' translation helper 7 | "helper_name": "_", 8 | 9 | // tap:i18n automatically separates the translation strings of each package to a 10 | // namespace dedicated to that package, which is used by the package's translation 11 | // function and helper. Use the namespace option to set a custom namespace for 12 | // the package. By using the name of another package you can use your package to 13 | // add to that package or modify its translations. You can also set the namespace to 14 | // "project" to add translations that will be available in the project level. 15 | "namespace": "project" 16 | } 17 | -------------------------------------------------------------------------------- /src/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'xolvio:md-blog', 3 | summary: 'A markdown powered blog with i18n and lots of cutomization options.', 4 | version: '0.5.2', 5 | git: 'https://github.com/xolvio/md-blog' 6 | }); 7 | 8 | Npm.depends({}); 9 | 10 | Package.on_use(function (api) { 11 | 12 | // APIs 13 | api.use(['meteor-platform@1.2.0', 'less@2.7.5']); 14 | api.use(['spiderable@1.0.5']); 15 | api.use(['reactive-var@1.0.3'], ['client', 'server']); 16 | api.use(['iron:router@1.0.0'], ['client', 'server']); 17 | api.use(['chuangbo:marked@0.3.5'], ['client', 'server']); 18 | api.use(['mrt:moment@2.8.1'], ['client', 'server']); 19 | api.use(['xolvio:hljs@0.0.1'], ['client']); 20 | api.use(['alanning:roles@1.2.13'], ['client', 'server']); 21 | api.use(['fortawesome:fontawesome@4.2.0_2'], ['client']); 22 | api.use(['tap:i18n@1.3.1'], ['client', 'server']); 23 | api.use(['ogourment:settings@0.1.4']); 24 | api.use(['email@1.0.5']); 25 | api.use(['meteorhacks:ssr@2.1.1'], ['server']); 26 | api.use(['edgee:slingshot@0.4.1'], ['client','server']); 27 | api.use(['cfs:dropped-event@0.0.10'], 'client'); 28 | api.use(['ccorcos:clientside-image-manipulation@1.0.3'], 'client'); 29 | 30 | // Common 31 | api.add_files(['common/blog-collections.js'], ['client', 'server']); 32 | api.add_files(['common/blog-paths.js'], ['client', 'server']); 33 | 34 | // package-tap.i18n must be loaded before the templates 35 | api.add_files("package-tap.i18n", ["client", "server"]); 36 | 37 | // Server 38 | api.add_files(['server/blog-server.js'], 'server'); 39 | 40 | // Client 41 | api.add_files(['client/blog-templates.html'], 'client'); 42 | api.add_files(['client/blog-client.js'], 'client'); 43 | api.add_files(['client/blog-picture.js'], 'client'); 44 | api.add_files(['client/blog-route.js'], 'client'); 45 | api.add_files(['client/blog.less'], 'client'); 46 | 47 | // List languages files so Meteor will watch them and rebuild package as they change. 48 | // Languages files must be loaded after the templates 49 | // otherwise the templates won't have the i18n capabilities (unless 50 | // you'll register them with tap-i18n yourself). 51 | api.add_files([ 52 | "i18n/en.i18n.json", 53 | "i18n/fr.i18n.json" 54 | ], ["client", "server"]); 55 | 56 | api.export('Blog'); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /src/server/blog-server.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | MeteorSettings.setDefaults({ public: { 6 | blog: { defaultLocale: "en" } 7 | }}, MeteorSettings.REQUIRED_IN_PROD); 8 | 9 | Meteor.publish('blog', function () { 10 | if (Roles.userIsInRole(this.userId, ['mdblog-author'])) { 11 | return Blog.find(); 12 | } else { 13 | return Blog.find({published: true}); 14 | } 15 | }); 16 | 17 | function _upsertBlogPost (blog) { 18 | blog.published = blog.published ? blog.published : false; 19 | blog.archived = blog.archived ? blog.archived : false; 20 | blog.slug = _getSlug(blog.title); 21 | 22 | // if no id was provided, this is a new blog entry so we should create an ID here to extract 23 | // a short ID before this blog is inserted into the DB 24 | var id = blog._id; 25 | if (!id) { 26 | id = new Meteor.Collection.ObjectID()._str; 27 | } else { 28 | delete blog._id; 29 | } 30 | blog.shortId = id.substring(0, 5); 31 | 32 | Blog.upsert(id, {$set: blog}); 33 | } 34 | 35 | function _sendEmail (blog) { 36 | 37 | var addresses = Meteor.users.find( 38 | { 'emails.address': { $ne: '' } } ).map( 39 | function(doc) { return doc.emails[0].address }); 40 | 41 | console.info("Sending email for '" + blog.title + "' to " 42 | + addresses.length + ' recipient(s):', addresses); 43 | 44 | var sender = Meteor.user().emails[0].address; 45 | Email.send({ 46 | to: sender, 47 | bcc: addresses, 48 | from: sender, 49 | subject: blog.title, 50 | html: SSR.render('publishEmail', { 51 | summary: blog.summary, 52 | url: getBlogPostUrl(blog), 53 | read_more: TAPi18n.__('read_more', {}, 54 | Meteor.settings.public.blog.defaultLocale) 55 | }) 56 | }); 57 | } 58 | 59 | function _removePost (blog) { Blog.remove(blog._id); } 60 | 61 | var _authorRoleRequired = function (func) { 62 | return function(blog) { 63 | if (Roles.userIsInRole(this.userId, ['mdblog-author'])) { 64 | func(blog); 65 | return blog; 66 | } else { 67 | throw new Meteor.Error(403, "Not authorized"); 68 | } 69 | } 70 | } 71 | 72 | Meteor.methods({ 73 | 'upsertBlog': _authorRoleRequired( _upsertBlogPost ), 74 | 'sendEmail': _authorRoleRequired( _sendEmail ), 75 | 'deleteBlog': _authorRoleRequired( _removePost ), 76 | 'mdBlogCount': function () { 77 | if (Roles.userIsInRole(this.userId, ['mdblog-author'])) { 78 | return Blog.find().count(); 79 | } else { 80 | return Blog.find({published: true}).count(); 81 | } 82 | } 83 | }); 84 | 85 | function _getSlug (title) { 86 | 87 | var replace = [ 88 | ' ', '#', '%', '"', ':', '/', '?', 89 | '^', '`', '[', ']', '{', '}', '<', '>', 90 | ';', '@', '&', '=', '+', '$', '|', ',' 91 | ]; 92 | 93 | var slug = title.toLowerCase(); 94 | for (var i = 0; i < replace.length; i++) { 95 | slug = _replaceAll(replace[i], '-', slug); 96 | } 97 | return slug; 98 | } 99 | 100 | function _replaceAll (find, replace, str) { 101 | return str.replace(new RegExp('\\' + find, 'g'), replace); 102 | } 103 | 104 | 105 | Meteor.startup(function () { 106 | if (!!process.env.AUTO_RESET && process.env.NODE_ENV === 'development') { 107 | Blog.remove({}); 108 | } 109 | if (Blog.find().count() === 0) { 110 | var locale = Meteor.settings.public.blog.defaultLocale; 111 | _upsertBlogPost({ 112 | published: true, 113 | archived: false, 114 | title: TAPi18n.__("blog_post_setup_title", null, locale), 115 | author: TAPi18n.__("blog_post_setup_author", null, locale), 116 | date: new Date().getTime(), 117 | summary: TAPi18n.__("blog_post_setup_summary", null, locale), 118 | content: TAPi18n.__("blog_post_setup_contents", null, locale) 119 | }); 120 | } 121 | }); 122 | 123 | })(); 124 | -------------------------------------------------------------------------------- /src/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "accounts-base", 5 | "1.1.2" 6 | ], 7 | [ 8 | "alanning:roles", 9 | "1.2.13" 10 | ], 11 | [ 12 | "application-configuration", 13 | "1.0.3" 14 | ], 15 | [ 16 | "autoupdate", 17 | "1.1.3" 18 | ], 19 | [ 20 | "base64", 21 | "1.0.1" 22 | ], 23 | [ 24 | "binary-heap", 25 | "1.0.1" 26 | ], 27 | [ 28 | "blaze", 29 | "2.0.3" 30 | ], 31 | [ 32 | "blaze-tools", 33 | "1.0.1" 34 | ], 35 | [ 36 | "boilerplate-generator", 37 | "1.0.1" 38 | ], 39 | [ 40 | "callback-hook", 41 | "1.0.1" 42 | ], 43 | [ 44 | "check", 45 | "1.0.2" 46 | ], 47 | [ 48 | "chuangbo:marked", 49 | "0.3.5" 50 | ], 51 | [ 52 | "ddp", 53 | "1.0.12" 54 | ], 55 | [ 56 | "deps", 57 | "1.0.5" 58 | ], 59 | [ 60 | "ejson", 61 | "1.0.4" 62 | ], 63 | [ 64 | "fastclick", 65 | "1.0.1" 66 | ], 67 | [ 68 | "follower-livedata", 69 | "1.0.2" 70 | ], 71 | [ 72 | "fortawesome:fontawesome", 73 | "4.2.0_2" 74 | ], 75 | [ 76 | "geojson-utils", 77 | "1.0.1" 78 | ], 79 | [ 80 | "html-tools", 81 | "1.0.2" 82 | ], 83 | [ 84 | "htmljs", 85 | "1.0.2" 86 | ], 87 | [ 88 | "http", 89 | "1.0.8" 90 | ], 91 | [ 92 | "id-map", 93 | "1.0.1" 94 | ], 95 | [ 96 | "iron:controller", 97 | "1.0.3" 98 | ], 99 | [ 100 | "iron:core", 101 | "1.0.3" 102 | ], 103 | [ 104 | "iron:dynamic-template", 105 | "1.0.3" 106 | ], 107 | [ 108 | "iron:layout", 109 | "1.0.3" 110 | ], 111 | [ 112 | "iron:location", 113 | "1.0.3" 114 | ], 115 | [ 116 | "iron:middleware-stack", 117 | "1.0.3" 118 | ], 119 | [ 120 | "iron:router", 121 | "1.0.3" 122 | ], 123 | [ 124 | "iron:url", 125 | "1.0.3" 126 | ], 127 | [ 128 | "jquery", 129 | "1.0.1" 130 | ], 131 | [ 132 | "json", 133 | "1.0.1" 134 | ], 135 | [ 136 | "launch-screen", 137 | "1.0.0" 138 | ], 139 | [ 140 | "less", 141 | "1.0.11" 142 | ], 143 | [ 144 | "livedata", 145 | "1.0.11" 146 | ], 147 | [ 148 | "localstorage", 149 | "1.0.1" 150 | ], 151 | [ 152 | "logging", 153 | "1.0.5" 154 | ], 155 | [ 156 | "meteor", 157 | "1.1.3" 158 | ], 159 | [ 160 | "meteor-platform", 161 | "1.2.0" 162 | ], 163 | [ 164 | "minifiers", 165 | "1.1.2" 166 | ], 167 | [ 168 | "minimongo", 169 | "1.0.5" 170 | ], 171 | [ 172 | "mobile-status-bar", 173 | "1.0.1" 174 | ], 175 | [ 176 | "mongo", 177 | "1.0.9" 178 | ], 179 | [ 180 | "mrt:moment", 181 | "2.8.1" 182 | ], 183 | [ 184 | "observe-sequence", 185 | "1.0.3" 186 | ], 187 | [ 188 | "ordered-dict", 189 | "1.0.1" 190 | ], 191 | [ 192 | "random", 193 | "1.0.1" 194 | ], 195 | [ 196 | "reactive-dict", 197 | "1.0.4" 198 | ], 199 | [ 200 | "reactive-var", 201 | "1.0.3" 202 | ], 203 | [ 204 | "reload", 205 | "1.1.1" 206 | ], 207 | [ 208 | "retry", 209 | "1.0.1" 210 | ], 211 | [ 212 | "routepolicy", 213 | "1.0.2" 214 | ], 215 | [ 216 | "service-configuration", 217 | "1.0.2" 218 | ], 219 | [ 220 | "session", 221 | "1.0.4" 222 | ], 223 | [ 224 | "spacebars", 225 | "1.0.3" 226 | ], 227 | [ 228 | "spacebars-compiler", 229 | "1.0.3" 230 | ], 231 | [ 232 | "spiderable", 233 | "1.0.5" 234 | ], 235 | [ 236 | "templating", 237 | "1.0.9" 238 | ], 239 | [ 240 | "tracker", 241 | "1.0.3" 242 | ], 243 | [ 244 | "ui", 245 | "1.0.4" 246 | ], 247 | [ 248 | "underscore", 249 | "1.0.1" 250 | ], 251 | [ 252 | "url", 253 | "1.0.2" 254 | ], 255 | [ 256 | "webapp", 257 | "1.1.4" 258 | ], 259 | [ 260 | "webapp-hashing", 261 | "1.0.1" 262 | ], 263 | [ 264 | "xolvio:hljs", 265 | "0.0.1" 266 | ] 267 | ], 268 | "pluginDependencies": [], 269 | "toolVersion": "meteor-tool@1.0.36", 270 | "format": "1.0" 271 | } --------------------------------------------------------------------------------