├── .babelrc ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── GETTING_STARTED.md ├── LICENCE ├── README.md ├── circle.yml ├── css ├── base.css ├── icon.eot └── icon.woff ├── data └── form.json ├── dist ├── team-directory.css └── team-directory.js ├── index.html ├── index.js ├── package.json ├── src ├── actions │ └── index.js ├── components │ ├── error.js │ ├── filter.js │ ├── form.js │ └── form_types │ │ ├── add.js │ │ ├── add_single.js │ │ ├── checkbox.js │ │ ├── native.js │ │ ├── radio.js │ │ ├── select.js │ │ └── textarea.js ├── constants │ └── action_types.js ├── containers │ ├── app.js │ ├── edit.js │ ├── index.js │ ├── new.js │ └── notfound.js ├── index.js ├── initial_state.js ├── modal_style.js ├── reducers │ └── index.js └── utils.js ├── team.json └── test └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "object-assign" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true 5 | }, 6 | "parser": "babel-eslint", 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [0], 12 | "react/prop-types": 1, 13 | "react/no-multi-comp": 2, 14 | "react/no-danger": 2, 15 | "react/display-name": [2, { "acceptTranspilerName": true }], 16 | "react/no-unknown-property": [2], 17 | "react/prop-types": 2, 18 | "react/require-extension": [2, { "extensions": [".js", ".jsx"] }], 19 | "react/self-closing-comp": [2], 20 | "no-alert": [0], 21 | "new-cap": [0], 22 | "quotes": [2, "single"], 23 | "eol-last": [0], 24 | "no-mixed-requires": [0], 25 | "camelcase": [0], 26 | "consistent-return": [0], 27 | "no-underscore-dangle": [0], 28 | "comma-spacing": [0], 29 | "key-spacing": [0], 30 | "no-use-before-define": [0] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env.sh 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.6.5 2 | 3 | - [bug] Fix bad output when exporting JSON to CSV 4 | 5 | ### v1.6.4 6 | 7 | - [bug] Fix cases where the response returned from GitHub is not a Base64 encoded 8 | string. 9 | 10 | ### v1.6.3 11 | 12 | - [bug] UTF-8 encode non-ASCII names in vCards [#22](https://github.com/mapbox/team-directory/pull/22) 13 | 14 | ### v1.6.2 15 | 16 | - [bug] Drop `toString` method on `defaultChecked` condition in radio elements 17 | - [ui] Better label to describe downloading all contacts on listing page 18 | 19 | ### v1.6.1 20 | 21 | - [bug] Better handling of `boolean` values 22 | - [ui] Clear the select form type after selection 23 | - [ui] Style autocomplete to better align with existing style 24 | 25 | ### v1.6.0 26 | 27 | - [Feature] Add a autocomplete select from defined list form type [#17](https://github.com/mapbox/team-directory/issues/17) 28 | - [Internal] Break form items into components [#13](https://github.com/mapbox/team-directory/issues/13) 29 | 30 | ### v1.5.0 31 | 32 | - [UI] Add dismiss links to `add` form fields [#14](https://github.com/mapbox/team-directory/pull/14) 33 | - [Feature] Add a `add-single` form field [#16](https://github.com/mapbox/team-directory/pull/16) 34 | 35 | ### v1.4.1 36 | 37 | - [UI] Style admin fields differently [#11](https://github.com/mapbox/team-directory/issues/11) 38 | 39 | ### v1.4.0 40 | 41 | - [feature] Add fuzzy filtering [#9](https://github.com/mapbox/team-directory/pull/9) 42 | - [bug] Prolong loading until team data is fetched [#8](https://github.com/mapbox/team-directory/issues/8) 43 | 44 | ### v1.3.1 45 | 46 | - [bug] User form values passed by parent to the form component were not updating as `componentWillReceiveProps` was not called. 47 | 48 | ### v1.3.0 49 | 50 | - [feature] Added `pushState` as an optional flag to enable push state paths. Uses a hash prefix if this is not set. 51 | 52 | ### v1.2.3 53 | 54 | - [bug] Missing basePath on conditioned filtering. 55 | 56 | ### v1.2.2 57 | 58 | - [bug] Added `basePath` to all `reRoute` paths. 59 | 60 | ### v1.2.1 61 | 62 | - [bug] Explicty pass `basePath` to all routes and use a base tag in index.html. 63 | 64 | ### v1.2.0 65 | 66 | - [feature] Added optional `basePath` parameter to support root paths other than `'/'`. 67 | 68 | ### v1.1.0 69 | 70 | - [feature] Added `user.edit` event 71 | - [ui] Loading state on all IO 72 | - [ui] Redirect to listing page after all user operations 73 | - [bug] Pass optional branch param to fetch parameters. 74 | 75 | ### v1.0.5 76 | 77 | - [bug] Fix error on `sorts` dispatching. 78 | 79 | ### v1.0.4 80 | 81 | - [bug] Corrected documentation and function call for `sorts`. 82 | 83 | ### v1.0.3 84 | 85 | - [bug] Corrected documentation and function calls for `listingTemplate` & `statsTemplate`. 86 | 87 | ### v1.0.2 88 | 89 | - [bug] Support branches when reading form & team data contents. 90 | 91 | ### v1.0.1 92 | 93 | - [bug] Deploy first release. 94 | 95 | ### v1.0.0 96 | 97 | - Initial commit. 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Developing 2 | 3 | - Clone a copy of this project 4 | - Change into the project directory from the terminal and install dependencies via `npm install`. 5 | - Replace all the credentials found in [index][] with your own. Read [Getting started][] to learn more. 6 | - Run `npm start` to start a server on `http://localhost:9966/`. 7 | 8 | [Getting started]: https://github.com/mapbox/team-directory/blob/master/GETTING_STARTED.md 9 | [index]: https://github.com/mapbox/team-directory/blob/master/index.html 10 | 11 | ## Tests 12 | 13 | To run tests locally, you'll need create a writeable file called `.env.sh` with 14 | the following contents: 15 | 16 | ```sh 17 | export GitHubToken="GITHUB TOKEN" 18 | export account="ACCOUNT NAME" 19 | export repo="REPO NAME" 20 | export team="TEAM FILENAME" 21 | export form="FORM FILENAME" 22 | ``` 23 | 24 | _Note:_ you can optionally add `export branch="BRANCH NAME"` if test data lies in a 25 | specific branch. 26 | 27 | Run tests via 28 | 29 | npm run test-local 30 | 31 | ## Deploying 32 | 33 | - `npm run build` 34 | - Add entry to [CHANGELOG](https://github.com/mapbox/team-directory/blob/master/CHANGELOG.md) 35 | - Update the version key in [package.json](https://github.com/mapbox/team-directory/blob/master/package.json#L3) 36 | - Commit and push 37 | - `git tag -a vX.X.X -m 'vX.X.X'` 38 | - `git push --tags` 39 | - `npm publish` 40 | -------------------------------------------------------------------------------- /GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | #### Table of contents 4 | 5 | - [Quick start](#quick-start) 6 | - [Initializing](#initializing) 7 | - [form.json](#formjson) 8 | - [form.json types](#formjson-types) 9 | - [Advanced configuration](#advanced-configuration) 10 | - [Sorts](#teamdirectorysorts) 11 | - [Validators](#teamdirectoryvalidators) 12 | - [Normalizers](#teamdirectorynormalizers) 13 | - [Listing Template](#teamdirectorylistingtemplate) 14 | - [Stats Template](#teamdirectorystatstemplate) 15 | - [Events](#events) 16 | 17 | ### Quick start 18 | 19 | ```html 20 |
21 | 22 | 23 | 32 | ``` 33 | 34 | FIll out the values above with your own credentials. An example configuration 35 | can be found [here](https://github.com/mapbox/team-directory/blob/master/index.html). 36 | 37 | ### Initializing 38 | 39 | ___`TeamDirectory(el, options)`___ 40 | - `el` (String or HTMLElement) of the container element the application should populate. 41 | - `options` are as follows: 42 | 43 | | Option | Value | Required | Default value | Description | 44 | | --- | --- | --- | --- | --- | 45 | | GitHubToken | String | ✓ | | A users GitHub token | 46 | | account | String | ✓ | | GitHub organization or account name | 47 | | repo | String | ✓ | | The repository where team & form documents are located | 48 | | team | String | ✓ | | the path and filename in `repo` where team data is written out to | 49 | | form | String | ✓ | | the path and filename in `repo` where form data is read from | 50 | | branch | String | | | Specify a specific branch found in `repo` | 51 | | pushState | Boolean | | `false`| Set to `true` to enable `history.pushstate` Defaults to hash prefixed paths. | 52 | | basePath | String | | `'/'`| Pass an alternate path team-directory should be built from | 53 | | filterKeys | Array | | `['github', 'fname']` | An array of keys as strings that correspond to a key property names found in your form data. | 54 | 55 | #### form.json 56 | 57 | Team Directory is designed for you to provide your own form data to meet your 58 | own needs. An example of a [form.json can be found here](https://github.com/mapbox/team-directory/blob/master/data/form.json) but at it's most basic the structure looks 59 | like this: 60 | 61 | ```json 62 | { 63 | "Basic information": [{ 64 | "key": "github", 65 | "label": "Github username", 66 | "required": true 67 | }, { 68 | "key": "birthday", 69 | "label": "Birthday", 70 | "type": "date" 71 | }] 72 | } 73 | ``` 74 | 75 | _A couple notes:_ 76 | 77 | - __`Basic information`__ is the section name the form field belongs to. You can use any 78 | arbitrary name here or break form fields into any number of sections. 79 | - __GitHub__ is required. This is used in the verification process to insure no 80 | duplicate users is created. 81 | 82 | As you can see from the example json above, each form field is represented 83 | as an object with specific key/value pairings. There are a few as follows: 84 | 85 | 86 | | Option | Value | Required | Description | 87 | | --- | --- | --- | --- | 88 | | key | String | ✓ | A unique key name | 89 | | label | String | | Form label shown above the field element | 90 | | required | Boolean | | If a form field is required the form won't submit for the user until a value has been passed. | 91 | | admin | Boolean | | Form field is only present if the user editing has `admin: true` set in their user object. | 92 | | fields | string | ✓ for some type attributes | Specific to checkboxes and radios, fields are an array of objects with `key` and `label` properties | 93 | | type | string | | If this value isnt provided, it defaults to 'text' See below for form types and their structures | 94 | 95 | #### form.json types 96 | 97 | ##### `add` 98 | 99 | ```json 100 | { 101 | "key": "other-links", 102 | "label": "Other links", 103 | "type": "add" 104 | } 105 | ``` 106 | 107 | A form field for adding multiple name/value pairings. Stored as an array of objects 108 | with `name` and `value` properties. 109 | 110 | ##### `add-single` 111 | 112 | ```json 113 | { 114 | "key": "tags", 115 | "label": "Add tags", 116 | "type": "add-single" 117 | } 118 | ``` 119 | 120 | A form field for adding multiple values. Stored as an array of strings. 121 | 122 | ##### `select` 123 | 124 | ```json 125 | { 126 | "key": "languages", 127 | "label": "Languages spoken", 128 | "type": "select", 129 | "options": [{ 130 | "label": "English", 131 | "key": "en" 132 | }, { 133 | "label": "Español", 134 | "key": "es" 135 | }, { 136 | "label": "German", 137 | "key": "de" 138 | }, { 139 | "label": "French", 140 | "key": "fr" 141 | }] 142 | } 143 | ``` 144 | 145 | A autocomplete form field for adding multiple values from a defined list. 146 | 147 | ##### `textarea` 148 | 149 | ```json 150 | { 151 | "key": "adress", 152 | "label": "Home address", 153 | "type": "textarea" 154 | { 155 | ``` 156 | 157 | `textarea` input type. 158 | 159 | ##### `checkbox` 160 | 161 | ```json 162 | { 163 | "key": "teams", 164 | "label": "Teams (check all that apply)", 165 | "required": true, 166 | "type": "checkbox", 167 | "fields": [{ 168 | "key": "business", 169 | "label": "Business" 170 | }, { 171 | "key": "design", 172 | "label": "Design" 173 | }, { 174 | "key": "engineering", 175 | "label": "Engineering" 176 | }, { 177 | "key": "operations", 178 | "label": "Operations" 179 | }, { 180 | "key": "support", 181 | "label": "Support" 182 | }] 183 | } 184 | ``` 185 | 186 | Checkbox input type. 187 | 188 | ##### `radio` 189 | 190 | ```json 191 | { 192 | "key": "office", 193 | "label": "Office", 194 | "type": "radio", 195 | "fields": [{ 196 | "key": "dc", 197 | "label": "DC" 198 | }, { 199 | "key": "sf", 200 | "label": "SF" 201 | }, { 202 | "key": "ayacucho", 203 | "label": "Peru" 204 | }, { 205 | "key": "bengaluru", 206 | "label": "India" 207 | }] 208 | } 209 | ``` 210 | 211 | Radio input type 212 | 213 | ##### `number` 214 | 215 | ```json 216 | { 217 | "key": "call", 218 | "label": "Mobile number", 219 | "type": "number" 220 | } 221 | ``` 222 | 223 | Number input type. 224 | 225 | ##### `date` 226 | 227 | ```json 228 | } 229 | "key": "birthday", 230 | "label": "Birthday", 231 | "type": "date" 232 | } 233 | ``` 234 | 235 | Date input type. 236 | 237 | ### Advanced configuration 238 | 239 | If you provide your own custom form data, you'll likely want to override 240 | existing functionality to suit your needs. 241 | 242 | #### `TeamDirectory.sorts` 243 | 244 | Provide your own custom sorting on the listings page. `sorts` should equal an 245 | array of objects with `key` & `sort` pairings. `Key` must must correspond to a 246 | key attribute in the form data and the `sort` function should return the sorted 247 | array when complete. 248 | 249 | ```js 250 | var directory = TeamDirectory(document.getElementById('app'), options); 251 | 252 | directory.sorts([ 253 | { 254 | key: 'date', 255 | sort: function(team) { 256 | return team.sort((a, b) => { 257 | return new Date(b.birthday).getTime() - new Date(a.birthday).getTime(); 258 | }); 259 | } 260 | }, { 261 | key: 'name', 262 | return team.sort((a, b) => { 263 | return a.localeCompare(b); 264 | }); 265 | } 266 | ]); 267 | ``` 268 | 269 | #### `TeamDirectory.validators` 270 | 271 | Custom validation that's called before a team member is created or updated. 272 | The validators function is passed two arguments: `obj` The team member object & 273 | `callback` A function that's called in your code with either a string messsage 274 | describing a validation error found or `null` (no error found). Team member 275 | data will not be submitted until validation passes. 276 | 277 | ```js 278 | var directory = TeamDirectory(document.getElementById('app'), options); 279 | 280 | directory.validators(function(obj, callback) { 281 | if (obj.office === 'other' && !obj.city) { 282 | return callback('If the office selected is other, please enter your city'); 283 | } 284 | 285 | // No validation errors if it gets here 286 | return callback(null); 287 | }); 288 | ``` 289 | 290 | #### `TeamDirectory.normalizers` 291 | 292 | Format/normalize fields a user before its submitted. The normalizer function is 293 | passed two arguments: `obj` The team member object & `callback` A function 294 | that's called at the end of the function containing the new normalized/formatted 295 | user object. Team member data will not be submitted until this callback is called. 296 | 297 | ```js 298 | var directory = TeamDirectory(document.getElementById('app'), options); 299 | 300 | directory.normalizers(function(obj, callback) { 301 | return callback(obj.map(function(data) { 302 | 303 | // Remove any capitalization from an entered username. 304 | data.username = data.username.toLowerCase(); 305 | return data; 306 | }); 307 | }); 308 | ``` 309 | 310 | #### `TeamDirectory.listingTemplate` 311 | 312 | Create a custom listing template for team members. The listingTemplate is a 313 | function passed one argument: `obj` the current user in a list drawn out to 314 | the main page. The function must return [jsx template](https://facebook.github.io/jsx/). 315 | 316 | ```js 317 | var directory = TeamDirectory(document.getElementById('app'), options); 318 | 319 | directory.listingTemplate(function(obj) { 320 | var fullName = obj.fname + ' ' + obj.lname; 321 | 322 | return ( 323 |
324 | {fullName} 325 | {obj.birthday} 326 |
327 | ); 328 | }); 329 | ``` 330 | 331 | #### `TeamDirectory.statsTemplate` 332 | 333 | Evaluate team user data and present a template of found statistics. The 334 | statsTemplate is passed one argument: `team` the team array of users. The 335 | function must return [jsx template](https://facebook.github.io/jsx/). If no 336 | statsTemplate is provided, the teamStats link and modal will not be present 337 | on the listing page. 338 | 339 | ```js 340 | var directory = TeamDirectory(document.getElementById('app'), options); 341 | 342 | directory.statsTemplate(function(team) { 343 | var length = team.length; 344 | var phones = team.filter(function(member) { 345 | return member.phone; 346 | }).length; 347 | 348 | return ( 349 |
350 |

Team stats

351 |

There are {length} total team members and {phones} have phones.

352 |
353 | ); 354 | }); 355 | ``` 356 | 357 | ### Events 358 | 359 | ___`TeamDirectory.on(type, function)`___ 360 | 361 | Clients can subscribe to events that happen in the application. 362 | 363 | ```js 364 | var directory = TeamDirectory(document.getElementById('app'), options); 365 | 366 | // Get team data when it's available on the page 367 | directory.on('load', function(ev) { 368 | console.log(ev.team); 369 | }); 370 | ``` 371 | 372 | Available types are as follows: 373 | 374 | - `load` 375 | - `user.created` 376 | - `user.editing` 377 | - `user.updated` 378 | - `user.removed` 379 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2015, Mapbox 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Team directory 2 | --- 3 | 4 | [![npm version](http://img.shields.io/npm/v/team-directory.svg)](https://npmjs.org/package/team-directory) [![Circle CI](https://circleci.com/gh/mapbox/team-directory.svg?style=svg)](https://circleci.com/gh/mapbox/team-directory) 5 | 6 | A read/write interface for team data managed on GitHub. 7 | 8 | ![demo](https://i.imgur.com/LdW1GCz.gif) 9 | 10 | ### Features 11 | 12 | - List filtering/sorting with stored params 13 | - [vCard](https://en.wikipedia.org/wiki/VCard) downloads (single or entire team) 14 | - Stats display based on team data 15 | - User/admin only editing access 16 | 17 | ##### Customization 18 | 19 | - Your own data. Provide a json document of form field objects and team 20 | directory renders each one into submittable fields for each user. 21 | - Listing display. Limit fields that are displayed on the main listing by opting 22 | for which ones are used in a template. 23 | - Stats display 24 | - Sorting 25 | - List filtering 26 | - Form validation 27 | - Value normalization 28 | 29 | ##### Light admin access 30 | 31 | If an admin key is set to true on a user they are granted additional features: 32 | 33 | - View/edit all users 34 | - Download all fields from the team list as a CSV document. 35 | 36 | --- 37 | 38 | ## ☛ [Getting started][] 39 | ## ☛ [Contributing][] 40 | 41 | [Contributing]: https://github.com/mapbox/team-directory/blob/master/CONTRIBUTING.md 42 | [Getting started]: https://github.com/mapbox/team-directory/blob/master/GETTING_STARTED.md 43 | [index]: https://github.com/mapbox/team-directory/blob/master/index.html 44 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - test-data 5 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | Reset ♥ 3 | http://meyerweb.com/eric/tools/css/reset/ 4 | v2.0 | 20110126 5 | License: none (public domain) 6 | ------------------------------------------------------- */ 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, sup, tt, var, 12 | b, u, i, center, 13 | dl, dt, dd, ol, ul, li, 14 | fieldset, form, label, legend, 15 | table, caption, tbody, tfoot, thead, tr, th, td, 16 | article, aside, canvas, details, embed, 17 | figure, figcaption, footer, header, hgroup, 18 | menu, nav, output, ruby, section, summary, 19 | time, mark, audio, video { 20 | margin:0; 21 | padding:0; 22 | border:0; 23 | font-size:100%; 24 | font:inherit; 25 | vertical-align:baseline; 26 | } 27 | /* HTML5 display-role reset for older browsers */ 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display:block; 31 | } 32 | body { line-height:1; } 33 | ol, ul { list-style:none; } 34 | blockquote, q { quotes:none; } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { content:''; content:none; } 37 | /* tables still need 'cellspacing="0"' in the markup */ 38 | table { border-collapse: collapse; border-spacing:0; } 39 | /* remember to define focus styles. Hee Haw */ 40 | :focus { outline:0; } 41 | 42 | *, *:after, *:before { 43 | -webkit-box-sizing:border-box; 44 | -moz-box-sizing:border-box; 45 | box-sizing:border-box; 46 | } 47 | 48 | /* Inline Elements: Defaults 49 | ------------------------------------------------------- */ 50 | body, 51 | input, 52 | textarea { 53 | color:#404040; 54 | color:rgba(0,0,0,0.75); 55 | font:16px/24px 'Open sans', 'Helvetica neue', Helvetica, sans-serif; 56 | -webkit-font-smoothing:antialiased; 57 | } 58 | 59 | body { background:#fff; } 60 | .dark { color:#fff; } 61 | 62 | h1, 63 | h2, 64 | h3, 65 | h4, 66 | h5, 67 | h6 { 68 | font-family:'Open Sans Bold', 'Helvetica neue', Helvetica, sans-serif; 69 | font-weight:normal; 70 | margin:0; 71 | } 72 | 73 | h1 { 74 | font-size:30px; 75 | line-height:40px; 76 | } 77 | h2 { 78 | font-size:22px; 79 | line-height:30px; 80 | padding-top: 5px; 81 | padding-bottom:5px; 82 | } 83 | 84 | h3 { 85 | font-size:15px; 86 | line-height:20px; 87 | } 88 | 89 | h4, h5, h6 { 90 | font-size:12px; 91 | line-height:20px; 92 | } 93 | 94 | p { margin-bottom:20px; } 95 | 96 | p:last-child { margin-bottom:0; } 97 | 98 | small, 99 | .small { 100 | font-size:12px; 101 | line-height:20px; 102 | letter-spacing:0; 103 | } 104 | 105 | small { display:block; } 106 | 107 | .strong, 108 | strong { 109 | font-weight:700; 110 | } 111 | 112 | /* links */ 113 | a, 114 | a code { 115 | color:#3887BE; 116 | text-decoration:none; 117 | } 118 | a:hover code, 119 | a:hover { color:#63b6e5; } 120 | a:focus { 121 | -webkit-box-shadow:inset 0 0 0 1px rgba(0,0,0,0.05); 122 | box-shadow:inset 0 0 0 1px rgba(0,0,0,0.05); 123 | } 124 | 125 | .dark a, 126 | a.dark, 127 | .dark a > code { color:rgba(255,255,255,.5); } 128 | a.dark.active, 129 | .dark a.active, 130 | a.dark:hover, 131 | .dark a:hover > code, 132 | .dark a:hover { color:white; } 133 | .dark a:focus { 134 | box-shadow:inset 0 0 0 1px rgba(255,255,255,0.05); 135 | } 136 | 137 | a.quiet { color:rgba(0,0,0,0.5); } 138 | a.quiet.active, a.quiet:hover { color:rgba(0,0,0,0.75); } 139 | 140 | /* Buttons */ 141 | button, 142 | .button, 143 | [type=button], 144 | [type=submit] { 145 | background-color:#3887be; 146 | text-align:center; 147 | color:#fff; 148 | display:inline-block; 149 | height:40px; 150 | margin:0px; 151 | padding:10px; 152 | position:relative; 153 | border:none; 154 | cursor:pointer; 155 | border-radius:3px; 156 | white-space:nowrap; 157 | text-overflow:ellipsis; 158 | /* Protects button metrics in .prose context */ 159 | line-height:20px; 160 | font-size:12px; 161 | -webkit-appearance:none; 162 | font-weight:700; 163 | 164 | /* The button element needs to be forced this */ 165 | -webkit-font-smoothing:antialiased; 166 | } 167 | 168 | button:hover, 169 | .button:hover, 170 | .button.active, 171 | [type=button]:hover, 172 | [type=submit]:hover { 173 | color:#fff; 174 | } 175 | 176 | button:focus, 177 | .button:focus, 178 | [type=submit]:focus, 179 | [type=button]:focus { 180 | -webkit-box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1); 181 | box-shadow: inset 0 0 0 3px rgba(0,0,0,0.1); 182 | } 183 | 184 | button:hover, 185 | .button:hover, 186 | .button.active, 187 | [type=submit]:hover, 188 | [type=button]:hover { 189 | background-color:#52a1d8; 190 | } 191 | button.button-secondary, 192 | .button.button-secondary { 193 | background-color:#8889cc; 194 | } 195 | button.button-secondary:hover, 196 | .button.button-secondary.active, 197 | .button.button-secondary:hover { 198 | background-color:#a4a4e5; 199 | } 200 | 201 | .button.disabled, 202 | .button:disabled, 203 | button.disabled, 204 | button:disabled, 205 | [type=submit]:disabled, 206 | [type=button]:disabled { 207 | background:transparent; 208 | border:1px dashed rgba(0,0,0,0.25); 209 | color:rgba(0,0,0,0.25); 210 | cursor:not-allowed; 211 | } 212 | 213 | .button.disabled:hover, 214 | .button:disabled:hover, 215 | button.disabled:hover, 216 | button:disabled:hover, 217 | [type=submit]:disabled:hover, 218 | [type=button]:disabled:hover { 219 | background:transparent; 220 | } 221 | 222 | .button.disabled:focus, 223 | .button:disabled:focus, 224 | button.disabled:focus, 225 | button:disabled:focus, 226 | [type=submit]:disabled:focus, 227 | [type=button]:disabled:focus { 228 | box-shadow:none; 229 | } 230 | 231 | .button.short { 232 | height:30px; 233 | padding-top:5px; 234 | padding-bottom:5px; 235 | vertical-align:middle; 236 | } 237 | 238 | /* Pill wrapper for joining buttons. */ 239 | /* Use to eliminate whitespace between buttons. */ 240 | .pill{ 241 | display:inline-block; 242 | } 243 | .pill > * { 244 | border-radius:0 0 0 0; 245 | border:0 solid white; 246 | border-left-width:1px; 247 | } 248 | .pill > *:first-of-type, 249 | .pill > *:first-child { 250 | border-radius:3px 0 0 3px; 251 | border-left-width:0; 252 | } 253 | .pill > *:last-of-type, 254 | .pill > *:last-child { 255 | border-radius:0 3px 3px 0; 256 | } 257 | 258 | /* Vertical pill if pill contents is full width . */ 259 | .pill > .col12 { 260 | border-bottom-width:1px; 261 | border-left-width:0; 262 | } 263 | .pill > .col12:first-child { 264 | border-radius:3px 3px 0 0; 265 | } 266 | .pill > .col12:last-child { 267 | border-radius:0 0 3px 3px; 268 | border-bottom:none; 269 | } 270 | .pill > .col12:only-child { 271 | border-radius: 3px; 272 | } 273 | 274 | .pill input[type=text] { 275 | border:1px solid rgba(0,0,0,0.1); 276 | border-radius:0; 277 | } 278 | 279 | .pill input[type=text]:focus { 280 | border:1px solid rgba(0,0,0,0.25); 281 | } 282 | 283 | /* Checkbox pill - use in conjunction with pill + button 284 | -------------------------------------------------- */ 285 | .checkbox-pill input[type=checkbox] { display: none;} 286 | .checkbox-pill input[type=checkbox] + *:before { background-color: rgba(0,0,0,0.25); border-radius: 2px;} 287 | .checkbox-pill input[type=checkbox]:not(:checked) + *:before { content: '';} 288 | .checkbox-pill input[type=checkbox]:checked + .button { background-color: #52a1d8;} 289 | .checkbox-pill input[type=checkbox]:checked + .button.quiet { background-color: rgba(0,0,0,.5);} 290 | .checkbox-pill input[type=checkbox]:checked + .button.loud { background-color: #a4a4e5;} 291 | .dark.checkbox-pill input[type=checkbox]:checked + .button.quiet, 292 | .dark .checkbox-pill input[type=checkbox]:checked + .button.quiet { background-color: rgba(255,255,255,0.25);} 293 | 294 | /* Radio pill - use in conjunction with pill + button 295 | -------------------------------------------------- */ 296 | .radio-pill input[type=radio] { display: none;} 297 | .radio-pill input[type=radio] + .button:before { overflow: hidden;} 298 | .radio-pill input[type=radio]:not(:checked) + .button:before { width: 0;} 299 | .radio-pill input[type=radio]:checked + .button { background-color: #52a1d8;} 300 | .radio-pill input[type=radio]:checked + .button.quiet { background-color: rgba(0,0,0,.5);} 301 | .radio-pill input[type=radio]:checked + .button.loud { background-color: #a4a4e5;} 302 | .dark.radio-pill input[type=radio]:checked + .button.quiet, 303 | .dark .radio-pill input[type=radio]:checked + .button.quiet { background-color: rgba(255,255,255,0.25);} 304 | 305 | /* Rounded toggle 306 | -------------------------------------------------- */ 307 | .rounded-toggle { 308 | margin-top:5px; 309 | margin-bottom:5px; 310 | padding:2px; 311 | border-radius:15px; 312 | vertical-align:middle; 313 | background:rgba(0,0,0,.1) 314 | } 315 | 316 | .dark .rounded-toggle { background: rgba(255,255,255,.1);} 317 | .rounded-toggle > * { 318 | cursor:pointer; 319 | vertical-align:top; 320 | display:inline-block; 321 | border-radius:16px; 322 | padding:3px 10px; 323 | font-size:12px; 324 | color:rgba(0,0,0,0.5); 325 | line-height:20px; 326 | } 327 | 328 | .rounded-toggle > *:hover { color: rgba(0,0,0,0.75); } 329 | .rounded-toggle input[type=radio] { display:none; } 330 | .rounded-toggle input[type=radio]:checked + label, 331 | .rounded-toggle .active { background: white; color: rgba(0,0,0,.5); } 332 | 333 | form fieldset { 334 | margin: 0 0 20px; 335 | } 336 | form fieldset:last-child { 337 | margin-bottom: 0; 338 | } 339 | 340 | form fieldset label { 341 | font:12px/20px 'Open Sans Bold', sans-serif; 342 | display:block; 343 | margin:0 0 5px; 344 | } 345 | 346 | form fieldset label.inline { margin-bottom: 0;} 347 | input, 348 | select, 349 | button { 350 | vertical-align:top; 351 | } 352 | textarea, 353 | input[type=password], 354 | input[type=text], 355 | input[type=date], 356 | input[type=email], 357 | input[type=number] { 358 | background-color:#fff; 359 | border:1px solid rgba(0,0,0,0.1); 360 | display:inline-block; 361 | height:40px; 362 | margin:0; 363 | color:rgba(0,0,0,.5); 364 | padding:10px; 365 | border-radius:0; 366 | -webkit-appearance:none; 367 | -webkit-transition:border-color .05s; 368 | -moz-transition:border-color .05s; 369 | -ms-transition:border-color .05s; 370 | transition:border-color .05s; 371 | } 372 | textarea:focus, 373 | input[type=password]:focus, 374 | input[type=text]:focus, 375 | input[type=date]:focus, 376 | input[type=email]:focus, 377 | input[type=number]:focus { 378 | outline:thin dotted\8; /* ie8 below */ 379 | border-color:rgba(0,0,0,0.25); 380 | color:#404040; 381 | } 382 | 383 | textarea { 384 | height:80px; 385 | max-width:none; 386 | overflow:auto; 387 | resize:none; 388 | } 389 | 390 | /* Special Form Components */ 391 | .with-icon { 392 | position:relative; 393 | } 394 | .with-icon input[type=text], 395 | .with-icon input[type=number] { 396 | padding-left:35px; 397 | } 398 | .with-icon span.icon { 399 | position:absolute; 400 | top:10px; 401 | left:10px; 402 | } 403 | 404 | /* Checkboxes */ 405 | .checkbox input[type=checkbox] { display:none; } 406 | .checkbox label { 407 | display:block; 408 | cursor:pointer; 409 | padding:10px; 410 | } 411 | 412 | /* Icons 413 | ------------------------------------------------------- */ 414 | @font-face { 415 | font-family:'icon'; 416 | src:url('icon.eot?v=19'); 417 | src:url('icon.eot?v=19#iefix') format('embedded-opentype'), 418 | url('icon.woff?v=19') format('woff'); 419 | font-weight:normal; 420 | font-style:normal; 421 | } 422 | 423 | .icon:before { 424 | font-family: 'icon'; 425 | content:''; 426 | display:inline-block; 427 | width:20px; 428 | height:20px; 429 | font-size: 20px; 430 | color: inherit; 431 | vertical-align:top; 432 | -webkit-background-size:4320px 60px; 433 | background-size:4320px 60px; 434 | speak: none; 435 | font-style: normal; 436 | font-weight: normal; 437 | font-variant: normal; 438 | text-transform: none; 439 | line-height: 1; 440 | /* Better Font Rendering =========== */ 441 | -webkit-font-smoothing: antialiased; 442 | -moz-osx-font-smoothing: grayscale; 443 | } 444 | .icon:before { 445 | margin-right:5px; 446 | } 447 | .icon.caret-down:before { 448 | content: "\e6a5"; 449 | } 450 | .icon.caret-left:before { 451 | content: "\e6a6"; 452 | } 453 | .icon.caret-right:before { 454 | content: "\e6a7"; 455 | } 456 | .icon.caret-up:before { 457 | content: "\e6a8"; 458 | } 459 | .icon.step-ramp:before { 460 | content: "\e69c"; 461 | } 462 | .icon.smooth-ramp:before { 463 | content: "\e69d"; 464 | } 465 | .icon.bug:before { 466 | content: "\e69e"; 467 | } 468 | .icon.bell:before { 469 | content: "\e69f"; 470 | } 471 | .icon.polygon:before { 472 | content: "\e600"; 473 | } 474 | .icon.minus-document:before { 475 | content: "\e601"; 476 | } 477 | .icon.plus-document:before { 478 | content: "\e602"; 479 | } 480 | .icon.check-document:before { 481 | content: "\e603"; 482 | } 483 | .icon.grid:before { 484 | content: "\e604"; 485 | } 486 | .icon.osm:before { 487 | content: "\e605"; 488 | } 489 | .icon.at:before { 490 | content: "\e606"; 491 | } 492 | .icon.history:before { 493 | content: "\e607"; 494 | } 495 | .icon.palette:before { 496 | content: "\e608"; 497 | } 498 | .icon.sun:before { 499 | content: "\e609"; 500 | } 501 | .icon.account:before { 502 | content: "\e60a"; 503 | } 504 | .icon.adjust-stroke:before { 505 | content: "\e60b"; 506 | } 507 | .icon.alert:before { 508 | content: "\e60c"; 509 | } 510 | .icon.android:before { 511 | content: "\e60d"; 512 | } 513 | .icon.antialias:before { 514 | content: "\e60e"; 515 | } 516 | .icon.apple:before { 517 | content: "\e60f"; 518 | } 519 | .icon.arrive:before { 520 | content: "\e610"; 521 | } 522 | .icon.arrowright:before { 523 | content: "\e611"; 524 | } 525 | .icon.bear-left:before { 526 | content: "\e612"; 527 | } 528 | .icon.bear-right:before { 529 | content: "\e613"; 530 | } 531 | .icon.bolt:before { 532 | content: "\e614"; 533 | } 534 | .icon.book:before { 535 | content: "\e615"; 536 | } 537 | .icon.bookmark:before { 538 | content: "\e616"; 539 | } 540 | .icon.brackets:before { 541 | content: "\e617"; 542 | } 543 | .icon.building:before { 544 | content: "\e618"; 545 | } 546 | .icon.cap-butt:before { 547 | content: "\e619"; 548 | } 549 | .icon.cap-round:before { 550 | content: "\e61a"; 551 | } 552 | .icon.cap-square:before { 553 | content: "\e61b"; 554 | } 555 | .icon.cart:before { 556 | content: "\e61c"; 557 | } 558 | .icon.check:before { 559 | content: "\e61d"; 560 | } 561 | .icon.clipboard:before { 562 | content: "\e61e"; 563 | } 564 | .icon.close:before { 565 | content: "\e61f"; 566 | } 567 | .icon.x:before { 568 | content: "\e61f"; 569 | } 570 | .icon.cloud:before { 571 | content: "\e620"; 572 | } 573 | .icon.compass:before { 574 | content: "\e621"; 575 | } 576 | .icon.contact:before { 577 | content: "\e622"; 578 | } 579 | .icon.creditcard:before { 580 | content: "\e623"; 581 | } 582 | .icon.cross-edge:before { 583 | content: "\e624"; 584 | } 585 | .icon.crosshair:before { 586 | content: "\e625"; 587 | } 588 | .icon.dasharray:before { 589 | content: "\e626"; 590 | } 591 | .icon.dashboard:before { 592 | content: "\e627"; 593 | } 594 | .icon.data:before { 595 | content: "\e628"; 596 | } 597 | .icon.depart:before { 598 | content: "\e629"; 599 | } 600 | .icon.document:before { 601 | content: "\e62a"; 602 | } 603 | .icon.down:before { 604 | content: "\e62b"; 605 | } 606 | .icon.duplicate:before { 607 | content: "\e62c"; 608 | } 609 | .icon.en:before { 610 | content: "\e62d"; 611 | } 612 | .icon.enter-roundabout:before { 613 | content: "\e62e"; 614 | } 615 | .icon.eye:before { 616 | content: "\e62f"; 617 | } 618 | .icon.facebook:before { 619 | content: "\e630"; 620 | } 621 | .icon.floppy:before { 622 | content: "\e631"; 623 | } 624 | .icon.folder:before { 625 | content: "\e632"; 626 | } 627 | .icon.nofolder:before { 628 | content: "\e6a9"; 629 | } 630 | .icon.font:before { 631 | content: "\e633"; 632 | } 633 | .icon.forward:before { 634 | content: "\e634"; 635 | } 636 | .icon.foursquare:before { 637 | content: "\e635"; 638 | } 639 | .icon.fullscreen:before { 640 | content: "\e636"; 641 | } 642 | .icon.gap-width:before { 643 | content: "\e637"; 644 | } 645 | .icon.github:before { 646 | content: "\e638"; 647 | } 648 | .icon.gl:before { 649 | content: "\e639"; 650 | } 651 | .icon.globe:before { 652 | content: "\e63a"; 653 | } 654 | .icon.graph:before { 655 | content: "\e63b"; 656 | } 657 | .icon.hand:before { 658 | content: "\e63c"; 659 | } 660 | .icon.harddrive:before { 661 | content: "\e63d"; 662 | } 663 | .icon.heart:before { 664 | content: "\e63e"; 665 | } 666 | .icon.help:before { 667 | content: "\e63f"; 668 | } 669 | .icon.home:before { 670 | content: "\e640"; 671 | } 672 | .icon.info:before { 673 | content: "\e641"; 674 | } 675 | .icon.inspect:before { 676 | content: "\e642"; 677 | } 678 | .icon.join-miter:before { 679 | content: "\e643"; 680 | } 681 | .icon.join-bevel:before { 682 | content: "\e644"; 683 | } 684 | .icon.join-round:before { 685 | content: "\e645"; 686 | } 687 | .icon.l-r-arrow:before { 688 | content: "\e646"; 689 | } 690 | .icon.land:before { 691 | content: "\e647"; 692 | } 693 | .icon.landuse:before { 694 | content: "\e648"; 695 | } 696 | .icon.layers:before { 697 | content: "\e649"; 698 | } 699 | .icon.leaflet:before { 700 | content: "\e64a"; 701 | } 702 | .icon.levels:before { 703 | content: "\e64b"; 704 | } 705 | .icon.lifebuoy:before { 706 | content: "\e64c"; 707 | } 708 | .icon.line-miter-limit:before { 709 | content: "\e64d"; 710 | } 711 | .icon.line-round-limit:before { 712 | content: "\e64e"; 713 | } 714 | .icon.link:before { 715 | content: "\e64f"; 716 | } 717 | .icon.linkedin:before { 718 | content: "\e650"; 719 | } 720 | .icon.lock:before { 721 | content: "\e651"; 722 | } 723 | .icon.logout:before { 724 | content: "\e652"; 725 | } 726 | .icon.mail:before { 727 | content: "\e653"; 728 | } 729 | .icon.mapbox:before { 730 | content: "\e654"; 731 | } 732 | .icon.marker:before { 733 | content: "\e655"; 734 | } 735 | .icon.icon-halo:before { 736 | content: "\e6a2"; 737 | } 738 | .icon.symbol-layer:before { 739 | content: "\e6aa"; 740 | } 741 | .icon.max-text-angle:before { 742 | content: "\e656"; 743 | } 744 | .icon.menu:before { 745 | content: "\e657"; 746 | } 747 | .icon.minus:before { 748 | content: "\e658"; 749 | } 750 | .icon.mobile:before { 751 | content: "\e659"; 752 | } 753 | .icon.mt:before { 754 | content: "\e65a"; 755 | } 756 | .icon.next:before { 757 | content: "\e65b"; 758 | } 759 | .icon.noeye:before { 760 | content: "\e65c"; 761 | } 762 | .icon.opacity:before { 763 | content: "\e65d"; 764 | } 765 | .icon.package:before { 766 | content: "\e65e"; 767 | } 768 | .icon.paint:before { 769 | content: "\e65f"; 770 | } 771 | .icon.pencil:before { 772 | content: "\e660"; 773 | } 774 | .icon.picture:before { 775 | content: "\e661"; 776 | } 777 | .icon.plus:before { 778 | content: "\e662"; 779 | } 780 | .icon.point-line:before { 781 | content: "\e663"; 782 | } 783 | .icon.point:before { 784 | content: "\e664"; 785 | } 786 | .icon.polyline:before { 787 | content: "\e665"; 788 | } 789 | .icon.prev:before { 790 | content: "\e666"; 791 | } 792 | .icon.printer:before { 793 | content: "\e667"; 794 | } 795 | .icon.raster:before { 796 | content: "\e668"; 797 | } 798 | .icon.redo:before { 799 | content: "\e669"; 800 | } 801 | .icon.refresh:before { 802 | content: "\e66a"; 803 | } 804 | .icon.return:before { 805 | content: "\e66b"; 806 | } 807 | .icon.rotate:before { 808 | content: "\e66c"; 809 | } 810 | .icon.rss:before { 811 | content: "\e66d"; 812 | } 813 | .icon.satellite:before { 814 | content: "\e66e"; 815 | } 816 | .icon.search:before { 817 | content: "\e66f"; 818 | } 819 | .icon.share:before { 820 | content: "\e670"; 821 | } 822 | .icon.sharp-left:before { 823 | content: "\e671"; 824 | } 825 | .icon.sharp-right:before { 826 | content: "\e672"; 827 | } 828 | .icon.sprocket:before { 829 | content: "\e673"; 830 | } 831 | .icon.stackoverflow:before { 832 | content: "\e674"; 833 | } 834 | .icon.star:before { 835 | content: "\e675"; 836 | } 837 | .icon.street:before { 838 | content: "\e676"; 839 | } 840 | .icon.text-align-bottom-center:before { 841 | content: "\e677"; 842 | } 843 | .icon.text-align-bottom-left:before { 844 | content: "\e678"; 845 | } 846 | .icon.text-align-bottom-right:before { 847 | content: "\e679"; 848 | } 849 | .icon.text-align-center-center:before { 850 | content: "\e67a"; 851 | } 852 | .icon.text-align-center-left:before { 853 | content: "\e67b"; 854 | } 855 | .icon.text-align-center-right:before { 856 | content: "\e67c"; 857 | } 858 | .icon.text-align-overlap:before { 859 | content: "\e67d"; 860 | } 861 | .icon.text-align-top-center:before { 862 | content: "\e67e"; 863 | } 864 | .icon.text-align-top-left:before { 865 | content: "\e67f"; 866 | } 867 | .icon.text-align-top-right:before { 868 | content: "\e680"; 869 | } 870 | .icon.text-halo:before { 871 | content: "\e6a3"; 872 | } 873 | .icon.text-halo-width:before { 874 | content: "\e681"; 875 | } 876 | .icon.text-ignore-placement:before { 877 | content: "\e682"; 878 | } 879 | .icon.text-justify-center:before { 880 | content: "\e683"; 881 | } 882 | .icon.text-justify-left:before { 883 | content: "\e684"; 884 | } 885 | .icon.text-justify-right:before { 886 | content: "\e685"; 887 | } 888 | .icon.text-letter-spacing:before { 889 | content: "\e686"; 890 | } 891 | .icon.text-line-height:before { 892 | content: "\e687"; 893 | } 894 | .icon.text-max-width:before { 895 | content: "\e688"; 896 | } 897 | .icon.text-padding:before { 898 | content: "\e689"; 899 | } 900 | .icon.text-rotate:before { 901 | content: "\e68a"; 902 | } 903 | .icon.text-size:before { 904 | content: "\e68b"; 905 | } 906 | .icon.tilemill:before { 907 | content: "\e68c"; 908 | } 909 | .icon.time:before { 910 | content: "\e68d"; 911 | } 912 | .icon.tooltip:before { 913 | content: "\e68e"; 914 | } 915 | .icon.transform-lowercase:before { 916 | content: "\e68f"; 917 | } 918 | .icon.transform-uppercase:before { 919 | content: "\e690"; 920 | } 921 | .icon.trash:before { 922 | content: "\e691"; 923 | } 924 | .icon.position:before { 925 | content: "\e6a4"; 926 | } 927 | .icon.turn-left:before { 928 | content: "\e692"; 929 | } 930 | .icon.turn-right:before { 931 | content: "\e693"; 932 | } 933 | .icon.twitter:before { 934 | content: "\e694"; 935 | } 936 | .icon.tx:before { 937 | content: "\e695"; 938 | } 939 | .icon.u-d-arrow:before { 940 | content: "\e696"; 941 | } 942 | .icon.u-turn:before { 943 | content: "\e697"; 944 | } 945 | .icon.undo:before { 946 | content: "\e698"; 947 | } 948 | .icon.up:before { 949 | content: "\e699"; 950 | } 951 | .icon.video:before { 952 | content: "\e69a"; 953 | } 954 | .icon.water:before { 955 | content: "\e69b"; 956 | } 957 | .icon.quotes:before { 958 | content: "\e6a0"; 959 | } 960 | .icon.number:before { 961 | content: "\e6a1"; 962 | } 963 | .icon.line:before { 964 | content: "\e6ab"; 965 | } 966 | .icon.fill:before { 967 | content: "\e6ac"; 968 | } 969 | 970 | /* Columns 971 | ------------------------------------------------------- */ 972 | .limiter { 973 | width:83.33%; 974 | max-width:1000px; 975 | margin-left:auto; 976 | margin-right:auto; 977 | } 978 | 979 | .col0 { float:left; width:04.1666%; } 980 | .col1 { float:left; width:08.3333%; } 981 | .col2 { float:left; width:16.6666%; } 982 | .col3 { float:left; width:25.0000%; } 983 | .col4 { float:left; width:33.3333%; } 984 | .col5 { float:left; width:41.6666%; } 985 | .col6 { float:left; width:50.0000%; } 986 | .col7 { float:left; width:58.3333%; } 987 | .col8 { float:left; width:66.6666%; } 988 | .col9 { float:left; width:75.0000%; } 989 | .col10 { float:left; width:83.3333%; } 990 | .col11 { float:left; width:91.6666%; } 991 | .col12 { width:100%; display:block; } 992 | 993 | .margin0 { margin-left:04.1666%; } 994 | .margin1 { margin-left:08.3333%; } 995 | .margin2 { margin-left:16.6666%; } 996 | .margin3 { margin-left:25.0000%; } 997 | .margin4 { margin-left:33.3333%; } 998 | .margin5 { margin-left:41.6666%; } 999 | .margin6 { margin-left:50.0000%; } 1000 | .margin7 { margin-left:58.3333%; } 1001 | .margin8 { margin-left:66.6666%; } 1002 | .margin9 { margin-left:75.0000%; } 1003 | .margin10 { margin-left:83.3333%; } 1004 | .margin11 { margin-left:91.6666%; } 1005 | .margin12 { margin-left:100.0000%; } 1006 | 1007 | /* reverse margins on right-floated elements */ 1008 | .margin0r { margin-right:04.1666%; } 1009 | .margin1r { margin-right:08.3333%; } 1010 | .margin2r { margin-right:16.6666%; } 1011 | .margin3r { margin-right:25.0000%; } 1012 | .margin4r { margin-right:33.3333%; } 1013 | .margin5r { margin-right:41.6666%; } 1014 | .margin6r { margin-right:50.0000%; } 1015 | .margin7r { margin-right:58.3333%; } 1016 | .margin8r { margin-right:66.6666%; } 1017 | .margin9r { margin-right:75.0000%; } 1018 | .margin10r { margin-right:83.3333%; } 1019 | .margin11r { margin-right:91.6666%; } 1020 | .margin12r { margin-right:100.0000%; } 1021 | 1022 | .row0 { height: 0px;} 1023 | .row1 { height: 40px;} 1024 | .row2 { height: 80px;} 1025 | .row3 { height: 120px;} 1026 | .row4 { height: 160px;} 1027 | .row5 { height: 200px;} 1028 | .row6 { height: 240px;} 1029 | .row7 { height: 280px;} 1030 | .row8 { height: 320px;} 1031 | .row9 { height: 360px;} 1032 | .row10 { height: 400px;} 1033 | .row11 { height: 440px;} 1034 | .row12 { height: 480px;} 1035 | .row13 { height: 520px;} 1036 | .row14 { height: 560px;} 1037 | .row15 { height: 600px;} 1038 | .row16 { height: 640px;} 1039 | 1040 | /* Padding 1041 | ------------------------------------------------------- */ 1042 | .pad0 { padding:5px; } 1043 | .pad1 { padding:10px; } 1044 | .pad2 { padding:20px; } 1045 | .pad4 { padding:40px; } 1046 | .pad8 { padding:80px; } 1047 | 1048 | .pad0x { padding-left: 5px; padding-right: 5px;} 1049 | .pad1x { padding-left: 10px; padding-right: 10px;} 1050 | .pad2x { padding-left: 20px; padding-right: 20px;} 1051 | .pad4x { padding-left: 40px; padding-right: 40px;} 1052 | .pad8x { padding-left: 80px; padding-right: 80px;} 1053 | 1054 | .pad0y { padding-top: 5px; padding-bottom: 5px;} 1055 | .pad1y { padding-top: 10px; padding-bottom: 10px;} 1056 | .pad2y { padding-top: 20px; padding-bottom: 20px;} 1057 | .pad4y { padding-top: 40px; padding-bottom: 40px;} 1058 | .pad8y { padding-top: 80px; padding-bottom: 80px;} 1059 | 1060 | /* Keylines 1061 | ------------------------------------------------------- */ 1062 | .keyline-all { border:1px solid rgba(0,0,0,0.10); } 1063 | .keyline-top { border-top:1px solid rgba(0,0,0,0.10); } 1064 | .keyline-right { border-right:1px solid rgba(0,0,0,0.10); } 1065 | .keyline-bottom { border-bottom:1px solid rgba(0,0,0,0.10); } 1066 | .keyline-left { border-left:1px solid rgba(0,0,0,0.10); } 1067 | 1068 | /* Pinned containers 1069 | ------------------------------------------------------- */ 1070 | .pin-top, 1071 | .pin-right, 1072 | .pin-bottom, 1073 | .pin-left, 1074 | .pin-topleft, 1075 | .pin-topright, 1076 | .pin-bottomleft, 1077 | .pin-bottomright { 1078 | position:absolute; 1079 | } 1080 | .pin-bottom { right:0; bottom:0; left:0; } 1081 | .pin-top { top:0; right:0; left:0; } 1082 | .pin-left { top:0; bottom:0; left:0; } 1083 | .pin-right { top:0; right:0; bottom:0; } 1084 | .pin-bottomright { bottom:0; right:0; } 1085 | .pin-bottomleft { bottom:0; left:0; } 1086 | .pin-topright { top:0; right:0; } 1087 | .pin-topleft { top:0; left:0; } 1088 | 1089 | /* Fixed containers 1090 | ------------------------------------------------------- */ 1091 | .fixed-top, 1092 | .fixed-right, 1093 | .fixed-bottom, 1094 | .fixed-left, 1095 | .fixed-topleft, 1096 | .fixed-topright, 1097 | .fixed-bottomleft, 1098 | .fixed-bottomright { 1099 | position:fixed; 1100 | } 1101 | .fixed-bottom { right:0; bottom:0; left:0; } 1102 | .fixed-top { top:0; right:0; left:0; } 1103 | .fixed-left { top:0; bottom:0; left:0; } 1104 | .fixed-right { top:0; right:0; bottom:0; } 1105 | .fixed-bottomright { bottom:0; right:0; } 1106 | .fixed-bottomleft { bottom:0; left:0; } 1107 | .fixed-topright { top:0; right:0; } 1108 | .fixed-topleft { top:0; left:0; } 1109 | 1110 | /* Container Animations/Transitions 1111 | -------------------------------------------------- */ 1112 | .animate { 1113 | -webkit-transition:all .125s; 1114 | -moz-transition:all .125s; 1115 | -ms-transition:all .125s; 1116 | transition:all .125s; 1117 | } 1118 | 1119 | .offcanvas-top { 1120 | -webkit-transform:translateY(-100%); 1121 | -moz-transform:translateY(-100%); 1122 | -ms-transform:translateY(-100%); 1123 | transform:translateY(-100%); 1124 | } 1125 | .offcanvas-right { 1126 | -webkit-transform:translateX(100%); 1127 | -moz-transform:translateX(100%); 1128 | -ms-transform:translateX(100%); 1129 | transform:translateX(100%); 1130 | } 1131 | .offcanvas-bottom { 1132 | -webkit-transform:translateY(100%); 1133 | -moz-transform:translateY(100%); 1134 | -ms-transform:translateY(100%); 1135 | transform:translateY(100%); 1136 | } 1137 | .offcanvas-left { 1138 | -webkit-transform:translateX(-100%); 1139 | -moz-transform:translateX(-100%); 1140 | -ms-transform:translateX(-100%); 1141 | transform:translateX(-100%); 1142 | } 1143 | 1144 | .offcanvas-top.active, 1145 | .offcanvas-bottom.active, 1146 | .offcanvas-top:target, 1147 | .offcanvas-bottom:target { 1148 | -webkit-transform:translateY(0); 1149 | -moz-transform:translateY(0); 1150 | -ms-transform:translateY(0); 1151 | transform:translateY(0); 1152 | } 1153 | 1154 | .offcanvas-left.active, 1155 | .offcanvas-right.active, 1156 | .offcanvas-left:target, 1157 | .offcanvas-right:target { 1158 | -webkit-transform:translateX(0); 1159 | -moz-transform:translateX(0); 1160 | -ms-transform:translateX(0); 1161 | transform:translateX(0); 1162 | } 1163 | 1164 | /* Markup free clearing 1165 | Details: http://www.positioniseverything.net/easyclearing.html 1166 | ------------------------------------------------------- */ 1167 | .clearfix { display:block; } 1168 | .clearfix:after { 1169 | content:'.'; 1170 | display:block; 1171 | height:0; 1172 | clear:both; 1173 | visibility:hidden; 1174 | } 1175 | 1176 | /* Color fills 1177 | ------------------------------------------------------- */ 1178 | .fill-blue { background-color:#3887be; } 1179 | .fill-orange { background-color:#f9886c; } 1180 | .fill-navy { background-color:#28353d; } 1181 | .fill-navy-dark { background-color:#222B30; } 1182 | .fill-purple { background-color:#8a8acb; } 1183 | .fill-light { background-color:#f8f8f8; } 1184 | .fill-red { background-color:#e55e5e; } 1185 | .fill-yellow { background-color:#f1f075; } 1186 | .fill-gray, 1187 | .fill-grey { background:#eee; } 1188 | .fill-darken0 { background-color:rgba(0,0,0,0.1); } 1189 | .fill-darken1 { background-color:rgba(0,0,0,0.25); } 1190 | 1191 | /* Additional Utility Classes 1192 | ------------------------------------------------------- */ 1193 | .fr { float:right; } 1194 | .fl { float:left; } 1195 | .dot { border-radius:50%; } 1196 | .light { color:#ccc; color: rgba(0,0,0,.3); } 1197 | .quiet { color:#7f7f7f; color: rgba(0,0,0,.5); } 1198 | .dark .quiet { color: #7f7f7f; color: rgba(255,255,255,.5);} 1199 | .center { text-align:center; } 1200 | .contain { position:relative; } 1201 | .clip { overflow:hidden; } 1202 | .hidden { display:none; } 1203 | .text-left { text-align:left; } 1204 | .text-right { text-align:right; } 1205 | .space > * { margin-right:5px; } 1206 | .space-bottom0 { margin-bottom: 5px;} 1207 | .space-bottom1 { margin-bottom: 10px;} 1208 | .space-bottom2 { margin-bottom: 20px;} 1209 | .space-bottom4 { margin-bottom: 40px;} 1210 | .space-bottom8 { margin-bottom: 80px;} 1211 | .space-left0 { margin-left: 5px;} 1212 | .space-left1 { margin-left: 10px;} 1213 | .space-left2 { margin-left: 20px;} 1214 | .space-left4 { margin-left: 40px;} 1215 | .space-left8 { margin-left: 80px;} 1216 | .space-right0 { margin-right: 5px;} 1217 | .space-right1 { margin-right: 10px;} 1218 | .space-right2 { margin-right: 20px;} 1219 | .space-right4 { margin-right: 40px;} 1220 | .space-right8 { margin-right: 80px;} 1221 | .hide-tablet, .hide-mobile { display:block; } 1222 | .show-tablet, .show-mobile { display:none; } 1223 | .show-mobile { display:none; } 1224 | img.inline, .inline { display:inline-block; } 1225 | .sprite.block,.block { display:block; } 1226 | .round { border-radius:3px; } 1227 | .round-top { border-radius:3px 3px 0 0; } 1228 | .round-right { border-radius:0 3px 3px 0; } 1229 | .round-bottom { border-radius:0 0 3px 3px; } 1230 | .round-left { border-radius:3px 0 0 3px; } 1231 | .round-topleft { border-top-left-radius: 3px;} 1232 | .round-bottomleft { border-bottom-left-radius: 3px;} 1233 | .round-topright { border-top-right-radius: 3px;} 1234 | .round-bottomright { border-bottom-right-radius: 3px;} 1235 | 1236 | .truncate { 1237 | text-overflow:ellipsis; 1238 | white-space:nowrap; 1239 | overflow:hidden; 1240 | } 1241 | 1242 | /* Mobile Layout 1243 | ------------------------------------------------------- */ 1244 | @media only screen and (max-width:640px) { 1245 | a:link { -webkit-tap-highlight-color:rgba(0,0,0,0); } 1246 | label .inline a { font-weight:normal; } 1247 | [type=submit] { width:100%; } 1248 | 1249 | .row1 { height:auto; min-height: 40px;} 1250 | .row2 { height:auto; min-height: 80px;} 1251 | .row3 { height:auto; min-height: 120px;} 1252 | .row4 { height:auto; min-height: 160px;} 1253 | .row5 { height:auto; min-height: 200px;} 1254 | .row6 { height:auto; min-height: 240px;} 1255 | .row7 { height:auto; min-height: 280px;} 1256 | .row8 { height:auto; min-height: 320px;} 1257 | .row9 { height:auto; min-height: 360px;} 1258 | .row10 { height:auto; min-height: 400px;} 1259 | .row11 { height:auto; min-height: 440px;} 1260 | .row12 { height:auto; min-height: 480px;} 1261 | .row13 { height:auto; min-height: 520px;} 1262 | .row14 { height:auto; min-height: 560px;} 1263 | .row15 { height:auto; min-height: 600px;} 1264 | .row16 { height:auto; min-height: 640px;} 1265 | 1266 | .col1, 1267 | .col2, 1268 | .col3, 1269 | .col4, 1270 | .col5, 1271 | .col6, 1272 | .col7, 1273 | .col8, 1274 | .col9, 1275 | .col10, 1276 | .col11, 1277 | .col12 { width:100%; max-width:100%; } 1278 | .margin0, 1279 | .margin1, 1280 | .margin2, 1281 | .margin3, 1282 | .margin4, 1283 | .margin5, 1284 | .margin6, 1285 | .margin7, 1286 | .margin8, 1287 | .margin9, 1288 | .margin10, 1289 | .margin11, 1290 | .margin12 { margin-left:0; } 1291 | .title { margin-bottom:10px; } 1292 | .space-bottom2, .space-bottom { margin-bottom:10px; } 1293 | .space-bottom4 { margin-bottom:20px; } 1294 | .hide-mobile { display:none!important; } 1295 | .show-mobile { display:block!important; } 1296 | 1297 | .pill:not(.mobile-cols) > * { 1298 | width:100%; 1299 | border-left-width:0; 1300 | border-bottom-width:1px; 1301 | } 1302 | .pill:not(.mobile-cols) > *:first-child, 1303 | .pill:not(.mobile-cols) > *:first-of-type { 1304 | border-radius:3px 3px 0 0; 1305 | } 1306 | .pill:not(.mobile-cols) > *:last-child, 1307 | .pill:not(.mobile-cols) > *:last-of-type { 1308 | border-bottom-width:0; 1309 | border-radius:0 0 3px 3px; 1310 | } 1311 | .tabs:not(.mobile-cols) > a { border-right-width:0; border-bottom-width:1px; } 1312 | .tabs:not(.mobile-cols) > a:last-child { border-bottom:none; } 1313 | 1314 | .mobile-cols > .col0 { float:left; width:04.1666%; } 1315 | .mobile-cols > .col1 { float:left; width:08.3333%; } 1316 | .mobile-cols > .col2 { float:left; width:16.6666%; } 1317 | .mobile-cols > .col3 { float:left; width:25.0000%; } 1318 | .mobile-cols > .col4 { float:left; width:33.3333%; } 1319 | .mobile-cols > .col5 { float:left; width:41.6666%; } 1320 | .mobile-cols > .col6 { float:left; width:50.0000%; } 1321 | .mobile-cols > .col7 { float:left; width:58.3333%; } 1322 | .mobile-cols > .col8 { float:left; width:66.6666%; } 1323 | .mobile-cols > .col9 { float:left; width:75.0000%; } 1324 | .mobile-cols > .col10 { float:left; width:83.3333%; } 1325 | .mobile-cols > .col11 { float:left; width:91.6666%; } 1326 | 1327 | .mobile-cols > .margin0 { margin-left:04.1666%; } 1328 | .mobile-cols > .margin1 { margin-left:08.3333%; } 1329 | .mobile-cols > .margin2 { margin-left:16.6666%; } 1330 | .mobile-cols > .margin3 { margin-left:25.0000%; } 1331 | .mobile-cols > .margin4 { margin-left:33.3333%; } 1332 | .mobile-cols > .margin5 { margin-left:41.6666%; } 1333 | .mobile-cols > .margin6 { margin-left:50.0000%; } 1334 | .mobile-cols > .margin7 { margin-left:58.3333%; } 1335 | .mobile-cols > .margin8 { margin-left:66.6666%; } 1336 | .mobile-cols > .margin9 { margin-left:75.0000%; } 1337 | .mobile-cols > .margin10 { margin-left:83.3333%; } 1338 | .mobile-cols > .margin11 { margin-left:91.6666%; } 1339 | .mobile-cols > .margin12 { margin-left:100.0000%; } 1340 | } 1341 | 1342 | /* Loading overlay 1343 | ------------------------------------------------------- */ 1344 | .loading:after, 1345 | .loading:before { 1346 | content:''; 1347 | display:block; 1348 | position:absolute; 1349 | z-index:10; 1350 | } 1351 | .loading:before { 1352 | background:transparent; 1353 | left:0; 1354 | top:0; 1355 | width:100%; 1356 | height:100%; 1357 | } 1358 | .loading:after { 1359 | background:rgba(0,0,0,.2) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICBpZD0ic3ZnMzEyMiIKICAgdmVyc2lvbj0iMS4xIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjUgcjEwMDQwIgogICB3aWR0aD0iMjQiCiAgIGhlaWdodD0iMjQiCiAgIHNvZGlwb2RpOmRvY25hbWU9ImxvYWRzb3VyY2UyLnN2ZyI+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhMzEyOCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgICA8ZGM6dGl0bGUgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczMxMjYiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxIgogICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAiCiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMTgyIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjcwOCIKICAgICBpZD0ibmFtZWR2aWV3MzEyNCIKICAgICBzaG93Z3JpZD0idHJ1ZSIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiCiAgICAgaW5rc2NhcGU6b2JqZWN0LW5vZGVzPSJ0cnVlIgogICAgIGlua3NjYXBlOnpvb209IjE2IgogICAgIGlua3NjYXBlOmN4PSI4Ljk3Nzk0NzciCiAgICAgaW5rc2NhcGU6Y3k9IjEwLjczMjQ3NiIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iNDgyIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIxMjciCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmczMTIyIgogICAgIHNob3dndWlkZXM9ImZhbHNlIgogICAgIGlua3NjYXBlOmd1aWRlLWJib3g9InRydWUiCiAgICAgaW5rc2NhcGU6b2JqZWN0LXBhdGhzPSJ0cnVlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIj4KICAgIDxpbmtzY2FwZTpncmlkCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBpZD0iZ3JpZDMxMzIiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiCiAgICAgICBzbmFwdmlzaWJsZWdyaWRsaW5lc29ubHk9InRydWUiCiAgICAgICBvcmlnaW54PSItMTQ4cHgiCiAgICAgICBvcmlnaW55PSItMzU4cHgiIC8+CiAgICA8c29kaXBvZGk6Z3VpZGUKICAgICAgIG9yaWVudGF0aW9uPSItMC43MDcxMDY3OCwwLjcwNzEwNjc4IgogICAgICAgcG9zaXRpb249IjEyLDEyIgogICAgICAgaWQ9Imd1aWRlNDEwNSIgLz4KICA8L3NvZGlwb2RpOm5hbWVkdmlldz4KICA8cGF0aAogICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTI7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICBkPSJNIDEyIDAgTCAxMiA1IEMgMTUuODY1OTkzIDUgMTkgOC4xMzQwMDY3IDE5IDEyIEwgMjQgMTIgQyAyNCA1LjM3MjU4MyAxOC42Mjc0MTcgMCAxMiAwIHogIgogICAgIGlkPSJwYXRoMzk1NiIgLz4KICA8cGF0aAogICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgc3R5bGU9Im9wYWNpdHk6MC40O2NvbG9yOiMwMDAwMDA7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxMjttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgIGQ9Ik0gMTIsMCBDIDUuMzcyNTgzLDAgMCw1LjM3MjU4MyAwLDEyIGMgMCwzLjE4MjU5OCAxLjI0OTU2Myw2LjI0OTU2MyAzLjUsOC41IDIuMjUwNDM3LDIuMjUwNDM3IDUuMzE3NDAyLDMuNSA4LjUsMy41IDMuMTgyNTk4LDAgNi4yNDk1NjMsLTEuMjQ5NTYzIDguNSwtMy41IEMgMjIuNzUwNDM3LDE4LjI0OTU2MyAyNCwxNS4xODI1OTggMjQsMTIgbCAtNSwwIGMgMCwzLjg2NTk5MyAtMy4xMzQwMDcsNyAtNyw3IEMgOC4xMzQwMDY4LDE5IDUsMTUuODY1OTkzIDUsMTIgNSw4LjEzNDAwNjcgOC4xMzQwMDY4LDUgMTIsNSB6IgogICAgIGlkPSJwYXRoMzE3NCIKICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjY2NjY2NjY2NjIiAvPgo8L3N2Zz4K) 50% 50% no-repeat; 1360 | left:50%; 1361 | top:50%; 1362 | margin:-20px 0 0 -20px; 1363 | width:40px; 1364 | height:40px; 1365 | border-radius:50%; 1366 | -webkit-animation: rotate 1s linear infinite; 1367 | -moz-animation: rotate 1s linear infinite; 1368 | -ms-animation: rotate 1s linear infinite; 1369 | animation: rotate 1s linear infinite; 1370 | } 1371 | 1372 | @-webkit-keyframes rotate { from { -webkit-transform: rotate(0); } to { -webkit-transform: rotate(360deg); } } 1373 | @-moz-keyframes rotate { from { -moz-transform: rotate(0); } to { -moz-transform: rotate(360deg); } } 1374 | @-ms-keyframes rotate { from { -ms-transform: rotate(0); } to { -ms-transform: rotate(360deg); } } 1375 | @keyframes rotate { from { transform: rotate(0); } to { transform: rotate(360deg); } } 1376 | -------------------------------------------------------------------------------- /css/icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cutting-room-floor/team-directory/d5f2585bf83a0976abb724a0e28898a67f50277f/css/icon.eot -------------------------------------------------------------------------------- /css/icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cutting-room-floor/team-directory/d5f2585bf83a0976abb724a0e28898a67f50277f/css/icon.woff -------------------------------------------------------------------------------- /data/form.json: -------------------------------------------------------------------------------- 1 | { 2 | "Basic information": [{ 3 | "key": "admin", 4 | "type": "hidden" 5 | }, { 6 | "key": "fname", 7 | "label": "First name", 8 | "required": true 9 | }, { 10 | "key": "lname", 11 | "label": "Last name", 12 | "required": true 13 | }, { 14 | "key": "email", 15 | "label": "Email", 16 | "required": true 17 | }, { 18 | "key": "sex", 19 | "label": "Gender", 20 | "type": "radio", 21 | "fields": [{ 22 | "key": "xy", 23 | "label": "Male" 24 | }, { 25 | "key": "xx", 26 | "label": "Female" 27 | }] 28 | }, { 29 | "key": "birthday", 30 | "label": "Birthday", 31 | "type": "date" 32 | }, { 33 | "key": "call", 34 | "label": "Mobile number", 35 | "type": "number" 36 | }, { 37 | "key": "admin", 38 | "label": "Grant admin access?", 39 | "admin": true, 40 | "type": "radio", 41 | "fields": [{ 42 | "key": true, 43 | "label": "True" 44 | }, { 45 | "key": false, 46 | "label": "False" 47 | }] 48 | }, { 49 | "key": "office", 50 | "label": "Office", 51 | "type": "radio", 52 | "fields": [{ 53 | "key": "dc", 54 | "label": "DC" 55 | }, { 56 | "key": "sf", 57 | "label": "SF" 58 | }, { 59 | "key": "ayacucho", 60 | "label": "Peru" 61 | }, { 62 | "key": "bengaluru", 63 | "label": "India" 64 | }] 65 | }, { 66 | "key": "homeaddress", 67 | "label": "Home Address", 68 | "type": "textarea" 69 | }, { 70 | "key": "teams", 71 | "label": "Teams (check all that apply)", 72 | "required": true, 73 | "type": "checkbox", 74 | "fields": [{ 75 | "key": "business", 76 | "label": "Business" 77 | }, { 78 | "key": "design", 79 | "label": "Design" 80 | }, { 81 | "key": "engineering", 82 | "label": "Engineering" 83 | }, { 84 | "key": "operations", 85 | "label": "Operations" 86 | }, { 87 | "key": "support", 88 | "label": "Support" 89 | }] 90 | }], 91 | "Social links": [{ 92 | "key": "twitter", 93 | "label": "Twitter username" 94 | }, { 95 | "key": "github", 96 | "label": "Github username", 97 | "required": true 98 | }, { 99 | "key": "foursquare", 100 | "label": "Foursquare username" 101 | }, { 102 | "key": "other-links", 103 | "type": "add", 104 | "label": "Other links" 105 | }] 106 | } 107 | -------------------------------------------------------------------------------- /dist/team-directory.css: -------------------------------------------------------------------------------- 1 | body { overflow-y:scroll; } 2 | form > fieldset { margin:0; } 3 | .space-right0 { margin-right:5px; } 4 | form fieldset:first-child, 5 | .no-last-keyline:last-child { border:none; } 6 | .question { cursor:help; } 7 | .square4 { 8 | width:40px; 9 | height:40px; 10 | } 11 | [data-tooltip] { 12 | cursor:pointer; 13 | position:relative; 14 | } 15 | [data-tooltip]:after { 16 | content:attr(data-tooltip); 17 | position:absolute; 18 | background:#404040; 19 | color:#fff; 20 | border-radius:3px; 21 | bottom:20px; 22 | display:none; 23 | padding:0 10px; 24 | width:auto; 25 | min-width:75px; 26 | left:0; 27 | margin-left:-10px; 28 | font-size:12px; 29 | height:30px; 30 | line-height:30px; 31 | text-align:center; 32 | } 33 | [data-tooltip]:before { 34 | content:''; 35 | display:block; 36 | border-left:5px solid transparent; 37 | border-right:5px solid transparent; 38 | border-top:5px solid #404040; 39 | display:none; 40 | position:absolute; 41 | bottom:15px; 42 | left:5px; 43 | } 44 | [data-tooltip]:hover:before, 45 | [data-tooltip]:hover:after { 46 | display:block; 47 | } 48 | .min-containment { 49 | min-height:600px; 50 | } 51 | .listing > *:last-child { border-bottom:none; } 52 | .colfifths { width:20%; float:left; } 53 | .text-alert { color:#f9886c; } 54 | 55 | /* When rendering groups react requires elements 56 | * to be wrapped in containers which breaks default 57 | * pill class rules. This brings them back */ 58 | .pill .react > * { 59 | border-radius:0 0 0 0; 60 | border:0 solid white; 61 | border-left-width:1px; 62 | } 63 | .pill .react:first-of-type > * { 64 | border-radius:3px 0 0 3px; 65 | border-left-width:0; 66 | } 67 | .pill .react:last-of-type > * { 68 | border-radius:0 3px 3px 0; 69 | } 70 | .pill .react:only-child > * { 71 | border-radius:3px; 72 | } 73 | 74 | .pill .set12:first-of-type > * { 75 | border-radius:3px 3px 0 0; 76 | } 77 | .pill .set12:last-of-type > * { 78 | border-radius:0 0 3px 3px; 79 | } 80 | .pill .set12 > * { 81 | text-align:left; 82 | width:100%; 83 | border-left-width:0; 84 | border-bottom-width:1px; 85 | } 86 | 87 | .loading:before { background-color:rgba(0,0,0,0.25); } 88 | .loading:before, 89 | .loading:after { position:fixed; } 90 | 91 | fieldset:target > input { 92 | border-color:#f9886c; 93 | } 94 | 95 | nav.primary a { 96 | display:inline-block; 97 | margin-right:10px; 98 | box-shadow:inset 0 -3px 0 transparent; 99 | } 100 | nav.primary a:hover { 101 | box-shadow:inset 0 -3px 0 rgba(0,0,0,0.10); 102 | } 103 | nav.primary a.active { 104 | box-shadow:inset 0 -3px 0 #142736; 105 | } 106 | 107 | .rounded-toggle label { 108 | cursor:pointer; 109 | vertical-align:top; 110 | display:inline-block; 111 | border-radius:16px; 112 | padding:3px 10px; 113 | font-size:12px; 114 | line-height:20px; 115 | } 116 | .rounded-toggle > span { 117 | padding:0; 118 | } 119 | 120 | .error-console { word-wrap:break-word; } 121 | 122 | /* react autosuggest */ 123 | .react-autosuggest__container { 124 | position:relative; 125 | } 126 | .react-autosuggest__input { 127 | width:100%; 128 | height:40px; 129 | padding:10px; 130 | border:1px solid rgba(0,0,0,0.25); 131 | border-radius:3px; 132 | } 133 | .react-autosuggest__input:focus { 134 | outline:none; 135 | } 136 | .react-autosuggest__container--open .react-autosuggest__input { 137 | border-bottom-left-radius:0; 138 | border-bottom-right-radius:0; 139 | } 140 | .react-autosuggest__suggestions-container { 141 | position:absolute; 142 | top:41px; 143 | width:100%; 144 | margin:0; 145 | padding:0; 146 | list-style-type:none; 147 | border:1px solid rgba(0,0,0,0.25); 148 | background-color:#fff; 149 | border-bottom-left-radius:3px; 150 | border-bottom-right-radius:3px; 151 | z-index:2; 152 | } 153 | .react-autosuggest__suggestion { 154 | cursor:pointer; 155 | padding:10px; 156 | } 157 | .react-autosuggest__suggestion--focused { 158 | background-color:rgba(0,0,0,0.1); 159 | } 160 | 161 | 162 | @media only screen and (max-width:640px) { 163 | .link a { padding:2.5px 0; } 164 | .pill:not(.mobile-cols) .react:first-of-type > * { 165 | border-radius:3px 3px 0 0; 166 | } 167 | .pill:not(.mobile-cols) .react:last-of-type > * { 168 | border-radius:0 0 3px 3px; 169 | } 170 | .pill:not(.mobile-cols) .react > * { 171 | width:100%; 172 | border-left-width:0; 173 | border-bottom-width:1px; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import TeamDirectory from './src'; 2 | 3 | function Directory(id, options) { 4 | return new TeamDirectory(id, options); 5 | } 6 | 7 | if (window && typeof module !== 'undefined' && module.exports) { 8 | window.TeamDirectory = module.exports = Directory; 9 | } else if (typeof module !== 'undefined' && module.exports) { 10 | module.exports = Directory; 11 | } else { 12 | window.TeamDirectory = Directory; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "team-directory", 3 | "version": "1.6.5", 4 | "description": "A rolodex for teams", 5 | "main": "index.js", 6 | "browserify": { 7 | "transform": [ 8 | "envify", 9 | "babelify" 10 | ] 11 | }, 12 | "scripts": { 13 | "start": "budo index.js --serve=dist/team-directory.js --live -d --pushstate", 14 | "build": "browserify index.js | uglifyjs -c -m > dist/team-directory.js", 15 | "test": "npm run lint && browserify test/test.js | smokestack -b firefox | tap-status", 16 | "test-local": "./.env.sh ; npm run lint && browserify test/test.js | smokestack -b firefox | tap-status", 17 | "lint": "eslint --no-eslintrc -c .eslintrc index.js src test" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+ssh://git@github.com/mapbox/team-directory.git" 22 | }, 23 | "keywords": [ 24 | "directory", 25 | "rolodex", 26 | "contacts" 27 | ], 28 | "author": "mapbox", 29 | "license": "BSD-3-Clause", 30 | "bugs": { 31 | "url": "https://github.com/mapbox/team-directory/issues" 32 | }, 33 | "homepage": "https://github.com/mapbox/team-directory#readme", 34 | "devDependencies": { 35 | "babel-core": "^5.8.25", 36 | "babel-eslint": "^4.1.5", 37 | "babel-plugin-object-assign": "^1.2.1", 38 | "babelify": "^6.3.0", 39 | "browserify": "^11.2.0", 40 | "budo": "^7.0.2", 41 | "envify": "^3.4.0", 42 | "eslint": "^1.7.2", 43 | "eslint-plugin-react": "^3.10.0", 44 | "smokestack": "^3.4.1", 45 | "tap-status": "^1.0.1", 46 | "tape": "^4.2.2", 47 | "uglify-js": "^2.6.1" 48 | }, 49 | "dependencies": { 50 | "filesaver.js": "^0.2.0", 51 | "fuzzy": "^0.1.1", 52 | "history": "1.12.5", 53 | "js-base64": "^2.1.9", 54 | "json-csv": "^1.3.0", 55 | "md5-jkmyers": "0.0.1", 56 | "octokat": "^0.4.13", 57 | "react": "^0.14.0", 58 | "react-autosuggest": "^3.6.0", 59 | "react-document-title": "^2.0.1", 60 | "react-dom": "^0.14.0", 61 | "react-modal": "^0.6.1", 62 | "react-redux": "^3.0.1", 63 | "react-router": "1.0.1", 64 | "redux": "^3.0.2", 65 | "redux-simple-router": "0.0.10", 66 | "redux-thunk": "^1.0.0", 67 | "vcf": "^1.1.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/action_types'; 2 | import Octokat from 'octokat'; 3 | import { Base64 } from 'js-base64'; 4 | import fuzzy from 'fuzzy'; 5 | let client, repo, config = {}; 6 | 7 | function setActor(actor) { 8 | return { 9 | type: types.ACTOR, 10 | actor 11 | }; 12 | } 13 | 14 | function setTeam(team) { 15 | return { 16 | type: types.TEAM, 17 | team 18 | }; 19 | } 20 | 21 | function setFilter(filterList) { 22 | return { 23 | type: types.FILTER_LIST, 24 | filterList 25 | }; 26 | } 27 | 28 | function setForm(form) { 29 | return { 30 | type: types.FORM, 31 | form 32 | }; 33 | } 34 | 35 | export function setMessage(message) { 36 | return { 37 | type: types.MESSAGE, 38 | message 39 | }; 40 | } 41 | 42 | export function setError(error) { 43 | return (dispatch) => { 44 | if (typeof error === 'object') error = JSON.parse(error.message).message; 45 | dispatch(isLoading(false)); 46 | dispatch({ 47 | type: types.ERROR, 48 | error 49 | }); 50 | } 51 | } 52 | 53 | export function setValidators(validators) { 54 | return { 55 | type: types.VALIDATORS, 56 | validators 57 | }; 58 | } 59 | 60 | export function setStatsTemplate(statsTemplate) { 61 | return { 62 | type: types.STATS_TEMPLATE, 63 | statsTemplate 64 | }; 65 | } 66 | 67 | export function setListingTemplate(listingTemplate) { 68 | return { 69 | type: types.LISTING_TEMPLATE, 70 | listingTemplate 71 | }; 72 | } 73 | 74 | export function setNormalizers(normalizers) { 75 | return { 76 | type: types.NORMALIZERS, 77 | normalizers 78 | }; 79 | } 80 | 81 | export function setOptions(options) { 82 | client = new Octokat({ token: options.GitHubToken }); 83 | repo = client.repos(options.account, options.repo); 84 | 85 | return { 86 | type: types.OPTIONS, 87 | options 88 | } 89 | } 90 | 91 | export function addUser(obj, cb) { 92 | return (dispatch, getState) => { 93 | const { options, team } = getState().directory; 94 | const config = {}; 95 | if (options.branch) config.ref = options.branch; 96 | 97 | dispatch(isLoading(true)); 98 | 99 | repo.contents(options.team).fetch(config).then((res) => { 100 | 101 | let dataFromGitHub = res.content ? JSON.parse(Base64.decode(res.content)) : res; 102 | dataFromGitHub.push(obj); // New record 103 | 104 | const payload = JSON.stringify(dataFromGitHub, null, 2) + '\n'; 105 | const putData = { 106 | message: 'Created ' + obj.github, 107 | content: Base64.encode(payload), 108 | sha: res.sha 109 | }; 110 | 111 | if (options.branch) putData.branch = options.branch; 112 | 113 | repo.contents(options.team).add(putData) 114 | .then(() => { 115 | dispatch(setFilter(dataFromGitHub)); 116 | dispatch(setTeam(dataFromGitHub)); 117 | dispatch(eventEmit('user.created', { user: obj })); 118 | dispatch(isLoading(false)); 119 | cb(null); 120 | }).catch((err) => { cb(err); }); 121 | }).catch((err) => { cb(err); }); 122 | } 123 | } 124 | 125 | export function updateUser(obj, cb) { 126 | return (dispatch, getState) => { 127 | const { options, team } = getState().directory; 128 | const config = {}; 129 | if (options.branch) config.ref = options.branch; 130 | dispatch(isLoading(true)); 131 | 132 | repo.contents(options.team).fetch(config).then((res) => { 133 | let dataFromGitHub = res.content ? JSON.parse(Base64.decode(res.content)) : res; 134 | dataFromGitHub = dataFromGitHub.map((d) => { 135 | if (obj.github.toLowerCase() === d.github.toLowerCase()) d = obj; 136 | return d; 137 | }); 138 | 139 | const payload = JSON.stringify(dataFromGitHub, null, 2) + '\n'; 140 | const putData = { 141 | message: 'Updated ' + obj.github, 142 | content: Base64.encode(payload), 143 | sha: res.sha 144 | }; 145 | 146 | if (options.branch) putData.branch = options.branch; 147 | 148 | repo.contents(options.team).add(putData) 149 | .then(() => { 150 | dispatch(setFilter(dataFromGitHub)); 151 | dispatch(setTeam(dataFromGitHub)); 152 | dispatch(eventEmit('user.updated', { user: obj })); 153 | dispatch(isLoading(false)); 154 | cb(null); 155 | }).catch((err) => { cb(err); }); 156 | }).catch((err) => { cb(err); }); 157 | } 158 | } 159 | 160 | export function removeUser(username, cb) { 161 | return (dispatch, getState) => { 162 | const { options, team } = getState().directory; 163 | const config = {}; 164 | if (options.branch) config.ref = options.branch; 165 | dispatch(isLoading(true)); 166 | 167 | repo.contents(options.team).fetch(config).then((res) => { 168 | let user; 169 | let dataFromGitHub = res.content ? JSON.parse(Base64.decode(res.content)) : res; 170 | 171 | dataFromGitHub = dataFromGitHub.filter((d) => { 172 | if (username.toLowerCase() === d.github.toLowerCase()) { 173 | user = d; 174 | return false; 175 | } else { 176 | return true; 177 | } 178 | }); 179 | 180 | const payload = JSON.stringify(dataFromGitHub, null, 2) + '\n'; 181 | const putData = { 182 | message: 'Removed ' + username, 183 | content: Base64.encode(payload), 184 | sha: res.sha 185 | }; 186 | 187 | if (options.branch) putData.branch = options.branch; 188 | 189 | repo.contents(options.team).add(putData) 190 | .then(() => { 191 | dispatch(setFilter(dataFromGitHub)); 192 | dispatch(setTeam(dataFromGitHub)); 193 | dispatch(eventEmit('user.removed', { user: user })); 194 | dispatch(isLoading(false)); 195 | cb(null); 196 | }).catch((err) => { cb(err); }); 197 | }).catch((err) => { cb(err); }); 198 | } 199 | } 200 | 201 | export function loadForm() { 202 | return (dispatch, getState) => { 203 | const { options } = getState().directory; 204 | const config = {}; 205 | if (options.branch) config.ref = options.branch; 206 | 207 | repo.contents(options.form).read(config) 208 | .then((res) => { 209 | res = JSON.parse(res); 210 | let data = []; 211 | for (const prop in res) { 212 | data.push({ 213 | section: prop, 214 | data: res[prop] 215 | }); 216 | } 217 | dispatch(setForm(data)); 218 | }) 219 | .catch((err) => { 220 | dispatch(setError(err)); 221 | dispatch(setForm([])); 222 | }); 223 | }; 224 | } 225 | 226 | export function loadUser(u) { 227 | return (dispatch, getState) => { 228 | const { team } = getState().directory; 229 | const user = team.filter((d) => { 230 | return u.toLowerCase() === d.github.toLowerCase(); 231 | })[0]; 232 | 233 | dispatch(eventEmit('user.editing', { user: user })); 234 | dispatch({ 235 | type: types.USER, 236 | user 237 | }); 238 | }; 239 | } 240 | 241 | export function loadTeam() { 242 | return (dispatch, getState) => { 243 | dispatch(isLoading(true)); 244 | const { options } = getState().directory; 245 | const config = {}; 246 | if (options.branch) config.ref = options.branch; 247 | 248 | repo.contents(options.team).read(config) 249 | .then((data) => { 250 | data = JSON.parse(data); 251 | 252 | client.user.fetch(config) 253 | .then((user) => { 254 | 255 | data.forEach((d) => { 256 | if (d.github.toLowerCase() === user.login.toLowerCase() && d.admin) { 257 | user.admin = true; 258 | } 259 | }); 260 | 261 | dispatch(setActor(user)); 262 | dispatch(setFilter(data)); 263 | dispatch(setTeam(data)); 264 | dispatch(eventEmit('load', { team: data, user: user })); 265 | dispatch(isLoading(false)); 266 | }); 267 | }) 268 | .catch((err) => { 269 | dispatch(setError(err)); 270 | dispatch(setTeam([])); 271 | dispatch(setFilter([])); 272 | }); 273 | }; 274 | } 275 | 276 | export function teamSort(sortIndex) { 277 | return (dispatch, getState) => { 278 | const { sorts, filterList } = getState().directory; 279 | dispatch(setFilter(sorts[sortIndex].sort(filterList))); 280 | } 281 | } 282 | 283 | export function teamFilter(query) { 284 | return (dispatch, getState) => { 285 | const { options, team } = getState().directory; 286 | query = decodeURIComponent(query.toLowerCase()); 287 | 288 | if (query.length > 2) { 289 | const results = fuzzy.filter(query, team, { 290 | extract: function(d) { 291 | const lookup = []; 292 | 293 | // Combine keys found in`options.filterKeys` 294 | options.filterKeys.forEach(function(key) { 295 | if (typeof key === 'object') { 296 | // TODO Keeping this around for now to avoid a breaking change. 297 | key.forEach(function(k) { 298 | lookup.push(d[k]); 299 | }); 300 | } 301 | lookup.push(d[key]); 302 | }); 303 | 304 | return lookup.join(' '); 305 | } 306 | }); 307 | 308 | dispatch(setFilter(results.map(function(d) { 309 | return d.original; 310 | }))); 311 | } else { 312 | dispatch(setFilter(team)); 313 | } 314 | } 315 | } 316 | 317 | function sorts(sorts) { 318 | return { 319 | type: types.SORTS, 320 | sorts 321 | }; 322 | } 323 | 324 | function sortKeys(sortKeys) { 325 | return { 326 | type: types.SORT_KEYS, 327 | sortKeys 328 | }; 329 | } 330 | 331 | export function setSorts(arr) { 332 | return (dispatch) => { 333 | dispatch(sorts(arr)); 334 | dispatch(sortKeys(arr.reduce((memo, sort) => { 335 | memo.push(sort.key); 336 | return memo; 337 | }, []))); 338 | } 339 | } 340 | 341 | export function dismissError() { 342 | return (dispatch) => { 343 | dispatch(setError('')); 344 | }; 345 | } 346 | 347 | export function dismissModal() { 348 | return (dispatch) => { 349 | dispatch(setMessage('')); 350 | }; 351 | } 352 | 353 | export function eventSubscribe(type, fn) { 354 | return (dispatch, getState) => { 355 | const { events } = getState().directory; 356 | events[type] = events[type] || []; 357 | events[type].push(fn); 358 | return { 359 | type: types.EVENTS, 360 | events 361 | }; 362 | } 363 | } 364 | 365 | export function eventEmit(type, data) { 366 | return (dispatch, getState) => { 367 | const { events } = getState().directory; 368 | 369 | if (!events[type]) { 370 | return { 371 | type: types.EVENTS, 372 | events 373 | } 374 | } 375 | 376 | const listeners = events[type].slice(); 377 | 378 | for (var i = 0; i < listeners.length; i++) { 379 | listeners[i].call(this, data); 380 | } 381 | } 382 | } 383 | 384 | export function isLoading(loading) { 385 | return { 386 | type: types.LOADING, 387 | loading 388 | }; 389 | } 390 | -------------------------------------------------------------------------------- /src/components/error.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class ErrorDialog extends Component { 4 | render() { 5 | const { error, dismissError } = this.props; 6 | const active = (error) ? 'active' : ''; 7 | if (active) window.setTimeout(dismissError, 3000); 8 | return ( 9 |
10 |
11 |
12 | {error} 13 |
14 |
15 |
16 | ); 17 | } 18 | } 19 | 20 | ErrorDialog.propTypes = { 21 | error: PropTypes.string.isRequired, 22 | dismissError: PropTypes.func.isRequired 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/filter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | export default class Filter extends Component { 5 | 6 | constructor(props, context) { 7 | super(props, context); 8 | } 9 | 10 | componentWillMount() { 11 | const { query, filter, sort, sortKeys } = this.props; 12 | if (query.filter) filter(query.filter); 13 | if (sortKeys.length) { 14 | if (query.sort) { 15 | sort(sortKeys.indexOf(query.sort)); 16 | } else { 17 | sort(sortKeys.indexOf(sortKeys[0])); 18 | } 19 | } 20 | } 21 | 22 | sort(e) { 23 | const { directory, sort, updatePath, query } = this.props; 24 | const index = encodeURIComponent(e.target.id); 25 | const value = encodeURIComponent(e.target.value); 26 | const bp = directory.options.basePath; 27 | updatePath(query.filter ? `${bp}?filter=${query.filter}&sort=${value}` : `${bp}?sort=${value}`); 28 | sort(index); 29 | } 30 | 31 | filter(e) { 32 | const { directory, filter, updatePath, query } = this.props; 33 | const value = encodeURIComponent(e.target.value); 34 | const bp = directory.options.basePath; 35 | updatePath(query.sort ? `${bp}?filter=${value}&sort=${query.sort}` : `${bp}?filter=${value}`); 36 | filter(value); 37 | } 38 | 39 | render() { 40 | const { query, sortKeys } = this.props; 41 | const filterValue = (query.filter) ? decodeURIComponent(query.filter) : ''; 42 | const activeSort = (query.sort) ? query.sort : sortKeys[0]; 43 | 44 | return ( 45 |
46 | 47 | 48 | {sortKeys.length &&
49 |
50 | {sortKeys.map((sort, i) => { 51 | return ( 52 | 53 | 54 | 55 | 56 | ); 57 | })} 58 |
59 |
} 60 |
61 | ); 62 | } 63 | } 64 | 65 | Filter.propTypes = { 66 | sortKeys: PropTypes.array.isRequired, 67 | updatePath: PropTypes.func.isRequired, 68 | query: PropTypes.object.isRequired, 69 | directory: PropTypes.object.isRequired, 70 | filter: PropTypes.func.isRequired, 71 | sort: PropTypes.func 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/form.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | // Form components 5 | import Textarea from './form_types/textarea'; 6 | import Native from './form_types/native'; 7 | import Radio from './form_types/radio'; 8 | import Checkbox from './form_types/checkbox'; 9 | import Add from './form_types/add'; 10 | import AddSingle from './form_types/add_single'; 11 | import Select from './form_types/select'; 12 | 13 | export default class Form extends Component { 14 | constructor(props, context) { 15 | super(props, context); 16 | const { user, data } = this.props; 17 | this.state = this.mapUser(user, data); 18 | } 19 | 20 | componentWillReceiveProps(next) { 21 | const { user, data } = next; 22 | if (!this.submission) this.setState(this.mapUser(user, data)); 23 | } 24 | 25 | mapUser(user, data) { 26 | // Map any existing user data to form properties. 27 | return data.reduce((memo, section) => { 28 | section.data.forEach((field) => { 29 | memo[field.key] = ''; 30 | if (user) { 31 | if (user[field.key] || typeof user[field.key] === 'boolean') { 32 | memo[field.key] = user[field.key]; 33 | } 34 | } 35 | }); 36 | return memo; 37 | }, {}); 38 | } 39 | 40 | exists(value) { 41 | var { team } = this.props; 42 | return team.some((user) => { 43 | return user.github.toLowerCase() === value.toLowerCase(); 44 | }); 45 | } 46 | 47 | formData() { 48 | const { data } = this.props; 49 | let formData = []; 50 | 51 | for (const p in data) { 52 | formData = formData.concat(data[p].data); 53 | } 54 | return formData; 55 | } 56 | 57 | onDelete(e) { 58 | const { onDelete, user } = this.props; 59 | e.preventDefault(); 60 | onDelete(); 61 | } 62 | 63 | onSubmit(e) { 64 | e.preventDefault(); 65 | this.submission = true; 66 | 67 | const data = this.state; 68 | const { setError, onSubmit, user, validators, normalizers } = this.props; 69 | 70 | // - Check that GitHub username does not exist. 71 | if (this.exists(data.github) && !user) { 72 | return setError(`User ${data.github} already exists.`); 73 | } 74 | 75 | // - Check all the required fields. 76 | const missingRequired = this.formData().filter((d) => { 77 | return d.required; 78 | }).filter((d) => { 79 | let contains; 80 | Object.keys(data).forEach(key => { 81 | // If the key we are looking falls under required 82 | if (key === d.key) { 83 | const value = data[key]; 84 | if (typeof value === 'string' && value || 85 | typeof value === 'boolean' || 86 | typeof value === 'object' && value.length) contains = true; 87 | } 88 | }); 89 | 90 | return !contains; 91 | }); 92 | 93 | if (missingRequired.length) { 94 | const requiredList = missingRequired.reduce((memo, req) => { 95 | memo.push('"' + req.label + '"'); 96 | return memo; 97 | }, []).join(', '); 98 | 99 | return setError(`Missing required fields ${requiredList}`); 100 | } 101 | 102 | // Validate 103 | validators(data, (err) => { 104 | if (err) return setError(err); 105 | 106 | // Normalize 107 | for (const key in data) { 108 | 109 | // Remove unfilled values 110 | if (!data[key]) { 111 | if (typeof data[key] !== 'boolean') delete data[key]; 112 | } 113 | 114 | if (typeof data[key] === 'object') { 115 | 116 | // Object structures (provided by "Add" inputs). 117 | // Ensure a value was filled. Otherwise remove it. 118 | data[key] = data[key].filter((d) => { 119 | let hasValue; 120 | for (let prop in d) { 121 | if (d[prop]) hasValue = true; 122 | } 123 | return hasValue; 124 | }); 125 | } 126 | } 127 | 128 | // Client normalization 129 | normalizers(data, (res) => { 130 | onSubmit(res); // Submit! 131 | }); 132 | }); 133 | } 134 | 135 | setFormValue(k, v) { 136 | var obj = {}; 137 | obj[k] = v; 138 | this.setState(obj); 139 | } 140 | 141 | render() { 142 | const { data, actor, user, onDelete } = this.props; 143 | const addFields = function(d, i) { 144 | const placeholder = (d.placeholder) ? d.placeholder : d.label ? d.label : ''; 145 | const type = (d.type) ? d.type : 'text'; 146 | const hidden = (type === 'hidden') ? 'hidden' : false; 147 | const defaultRenderer = (type === 'text' || 148 | type === 'date' || 149 | type === 'hidden' || 150 | type === 'number'); 151 | 152 | if (d.admin && !actor.admin) return; 153 | 154 | return ( 155 |
156 | 163 | {type === 'textarea' &&