18 |
19 | It looks like nothing was found at this location.
20 | Maybe try searching for what you need?
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default NotFound;
29 |
--------------------------------------------------------------------------------
/server/posts/2016-03-31__grommet-training-series-part-1-getting-started-with-grommet/content.md:
--------------------------------------------------------------------------------
1 | Our own Grommet developer, Alan Souza, recently kicked off the Grommet training series with our first tutorial. In it, Alan explains how viewers can get started with Grommet.
2 |
3 | By the end of this video you will be able to:
4 |
5 | * Install Grommet using Node/NPM
6 | * Create an initial application structure using Grommet CLI
7 | * Create a simple to-do app
8 | * Test Accessibility in your newly created app
9 |
10 | Note: This video is intended for viewers with knowledge in Javascript, Node.JS and [React](/post/2016/02/04/why-we-chose-react).
11 |
12 |
13 |
14 | Stay tuned for more Grommet courses and be sure to provide us with any feedback or questions, as well as any topics you would like covered!
--------------------------------------------------------------------------------
/src/js/components/manage/ManageDeletePost.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP
2 |
3 | import React, { PropTypes } from 'react';
4 | import Paragraph from 'grommet/components/Paragraph';
5 | import LayerForm from 'grommet-templates/components/LayerForm';
6 |
7 | const ManageDeletePost = (props) => {
8 | const { post, onCancel, onDelete } = props;
9 | return (
10 |
12 |
16 |
17 | );
18 | };
19 |
20 | ManageDeletePost.propTypes = {
21 | post: PropTypes.object.isRequired,
22 | onCancel: PropTypes.func.isRequired,
23 | onDelete: PropTypes.func.isRequired
24 | };
25 |
26 | export default ManageDeletePost;
27 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 | import 'whatwg-fetch';
3 | import { polyfill as promisePolyfill } from 'es6-promise';
4 | promisePolyfill();
5 |
6 | import '../scss/index.scss';
7 |
8 | import React from 'react';
9 | import { render } from 'react-dom';
10 | import { match } from 'react-router';
11 |
12 | import routes from './routes';
13 | import history from './RouteHistory';
14 | import BlogContext from './BlogContext';
15 |
16 | let element = document.getElementById('content');
17 |
18 | let asyncData;
19 | let asyncDataNode = document.getElementById('asyncData');
20 | if (asyncDataNode) {
21 | asyncData = JSON.parse(asyncDataNode.textContent);
22 | }
23 |
24 | match({ history, routes }, (error, redirectLocation, renderProps) => {
25 | render(
26 | ,
27 | element
28 | );
29 | });
30 |
31 | document.body.classList.remove('loading');
32 |
--------------------------------------------------------------------------------
/src/js/components/manage/ManageCancelChangePost.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP
2 |
3 | import React, { PropTypes } from 'react';
4 | import Paragraph from 'grommet/components/Paragraph';
5 | import LayerForm from 'grommet-templates/components/LayerForm';
6 |
7 | const ManageCancelChangePost = (props) => {
8 | const { post, onCancel, onConfirm } = props;
9 | return (
10 |
12 |
17 |
18 | );
19 | };
20 |
21 | ManageCancelChangePost.propTypes = {
22 | post: PropTypes.object.isRequired,
23 | onCancel: PropTypes.func.isRequired,
24 | onConfirm: PropTypes.func.isRequired
25 | };
26 |
27 | export default ManageCancelChangePost;
28 |
--------------------------------------------------------------------------------
/grommet-toolbox.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 |
4 | export default {
5 | copyAssets: [
6 | 'src/index.html',
7 | {
8 | asset: 'src/img/**',
9 | dist: 'dist/img/'
10 | },
11 | {
12 | asset: 'node_modules/grommet/img/shortcut-icon.png',
13 | dist: 'dist/img/'
14 | },
15 | {
16 | asset: 'node_modules/grommet/img/mobile-app-icon.png',
17 | dist: 'dist/img/'
18 | }
19 | ],
20 | jsAssets: ['src/js/**/*.js'],
21 | mainJs: 'src/js/index.js',
22 | mainScss: 'src/scss/index.scss',
23 | devServerPort: 9070,
24 | devServerProxy: {
25 | "/api/post/*": 'http://localhost:8070'
26 | },
27 | webpack: {
28 | resolve: {
29 | root: [
30 | path.resolve(__dirname, './node_modules')
31 | ]
32 | },
33 | plugins: [
34 | new webpack.ProvidePlugin({
35 | 'fetch': 'exports?self.fetch!whatwg-fetch'
36 | })
37 | ]
38 | },
39 | alias: {
40 | 'grommet/scss': path.resolve(__dirname, '../grommet/src/scss'),
41 | 'grommet': path.resolve(__dirname, '../grommet/src/js')
42 | },
43 | devPreprocess: ['set-webpack-alias'],
44 | scsslint: true
45 | };
46 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: '5'
3 | sudo: false
4 | before_script: npm install -g gulp
5 | script: gulp dist
6 | after_success:
7 | - |
8 | if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = false ]; then
9 | git config credential.helper "store --file=.git/credentials"
10 | echo "https://${GH_TOKEN}:@github.com" > .git/credentials
11 | git config --global user.name "Grommet Community Bot"
12 | git config --global user.email "asouza@hp.com"
13 | gulp release:heroku
14 | fi
15 | env:
16 | global:
17 | secure: ioEl3x6z0xdOe6Lxg0DdHzcvvZ00iZrqBTOPYcEVRSOcM4qaN+ffHOpZ1fBY5ZvMqrUWz7XPZiSjPh8pHZpvUBYC0goEZoqQibSxMRzM8PPvpT8ABJ6qYHgG3EQ+8Uc5xk68bWVBclo0JxCHGfRcXk7fGb3tH7CZ4hAWdBu9p7eazmddBsbEM6ASdaSeklapLBS+aqlUYbfkxIi7MywKSHUOCgZZ3GRWVKz2zPEugBm5yeOADAGuas8qBb3xDGa+s8Zja8a0ZvCgMMK5gbgDN3r7qW+Kd4lQzCqGRRe3Md6kwIz9h1diQoxXsC7DIjKmn+/BoJ1xdUbm6rX+2K1sqwzwC7+q7ooKpPva8BwSVZv5o4BCYQjmWq++IIT5aYnWROHkH+14h91rJprQIDtWn4rldB8bOlt5HYTX0x7OQNCi3Xopw3QF/h3DnWhjiPU92f2e35wc+LUozM+gqGjkKOsH9O8O3Fd4+D9/Ae5gS2o5l0woeiHUtUOtm+TyAwEh7E/bOAxp/FJvYKz3qbLxmrXnGTwu398HXS3skY4YTXyKKx7mGSwAjUNiMEZgX7tOAZb3g/JX7XIPKXsO1BEhYTPXX3QCZ3G9iISqGg3x4c/Gl2Xhu+RsxZeF/zXgeB4YzIi3NDF4FGwZWErYQZkQBxRdCxdXJdh3105Fsjcrjvs=
18 |
--------------------------------------------------------------------------------
/src/js/components/manage/ManageAddPost.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2015 Hewlett-Packard Development Company, L.P.
2 |
3 | import React, { Component } from 'react';
4 |
5 | import PostForm from './PostForm';
6 |
7 | import store from '../../store';
8 | import history from '../../RouteHistory';
9 |
10 | export default class ManageAddPost extends Component {
11 | constructor () {
12 | super();
13 |
14 | this._onAddPost = this._onAddPost.bind(this);
15 | this._onAddPostSucceed = this._onAddPostSucceed.bind(this);
16 | this._onAddPostFailed = this._onAddPostFailed.bind(this);
17 |
18 | this.state = {
19 | error: undefined
20 | };
21 | }
22 |
23 | _onAddPost (post) {
24 | store.addPost(post).then(
25 | this._onAddPostSucceed, this._onAddPostFailed
26 | );
27 | }
28 |
29 | _onAddPostSucceed () {
30 | history.push('/manage');
31 | }
32 |
33 | _onAddPostFailed () {
34 | this.setState({
35 | error: 'Could not add post, please try again.'
36 | });
37 | }
38 |
39 | render () {
40 | return (
41 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/server/posts/2016-04-28__grommet-062-released/content.md:
--------------------------------------------------------------------------------
1 | Grommet 0.6.2 has been released! This latest version includes the following changes:
2 |
3 | * **Enhanced Components:** Article, Box, Button, Chart, CheckBox, Distribution, FormField, Header, Image, Label, Layer, Menu, Meter, Paragraph, SkipLinks, Tags, Tile, Video
4 | * **New Components:** Markdown, WorldMap, SocialShare, Quote, Value
5 | * New set of icons
6 | * Removed deprecated features
7 | * Added support for grommet-toolbox
8 | * Accessibility improvements to Icons
9 | * Added tint (t) option to colored box
10 |
11 | It’s great to see Grommet’s momentum building month after month. It was a solid month for contributions with 235 commits and 42 pull requests accepted. Let’s put our virtual hands together for our amazing contributors: [@allisonday](https://github.com/codeswan), [@jackie](https://github.com/jwijay), [@eabay](https://github.com/eabay), [@son](https://github.com/phuson), [@nickvanmeter](https://github.com/nickjvm), [@primozs](https://github.com/primozs), [@alex.mejias](https://github.com/karatechops), [@kentsalcedo](https://github.com/kentsalcedo).
12 |
13 | P.S: Sorry about skipping 0.6.0 and 0.6.1. We are still facing intermittent issues on NPM with missing icons. 0.6.2 has been tested and is already being used in production.
14 |
--------------------------------------------------------------------------------
/src/js/routes.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import Blog from './Blog';
4 | import Home from './Home';
5 | import Archive from './components/Archive';
6 | import NotFound from './components/NotFound';
7 | import Post from './components/Post';
8 | import Search from './components/Search';
9 | import Manage from './components/manage/Manage';
10 | import ManageAddPost from './components/manage/ManageAddPost';
11 | import ManageEditPost from './components/manage/ManageEditPost';
12 |
13 | export default [
14 | { path: '/', component: Blog,
15 | indexRoute: { component: Home },
16 | childRoutes: [
17 | {
18 | path: 'archive', component: Archive,
19 | childRoutes: [
20 | { path: 'author/:authorName' },
21 | { path: 'tag/:tagName' },
22 | { path: ':year' },
23 | { path: ':year/:month' },
24 | { path: ':year/:month/:day' }
25 | ]
26 | },
27 | { path: 'post/*', component: Post },
28 | { path: 'search', component: Search },
29 | { path: 'manage', component: Manage },
30 | { path: 'manage/post/add', component: ManageAddPost },
31 | { path: 'manage/post/edit/*', component: ManageEditPost },
32 | { path: '*', component: NotFound }
33 | ]
34 | }
35 | ];
36 |
--------------------------------------------------------------------------------
/src/js/components/manage/Header.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component } from 'react';
4 | import { Link } from 'react-router';
5 |
6 | import Anchor from 'grommet/components/Anchor';
7 | import Box from 'grommet/components/Box';
8 | import Heading from 'grommet/components/Heading';
9 | import Header from 'grommet/components/Header';
10 | import Title from 'grommet/components/Title';
11 | import GrommetLogo from 'grommet/components/icons/Grommet';
12 |
13 | import Add from 'grommet/components/icons/base/Add';
14 |
15 | import history from '../../RouteHistory';
16 |
17 | export default class BlogHeader extends Component {
18 |
19 | _onAddPost (event) {
20 | event.preventDefault();
21 | history.push('/manage/post/add');
22 | }
23 |
24 | render () {
25 |
26 | const logo = (
27 |
28 |
29 |
30 | Blog
31 |
32 |
33 | );
34 |
35 | return (
36 |
38 | {logo}
39 |
40 | } onClick={this._onAddPost}
41 | a11yTitle='Add Post' />
42 |
43 |
44 | );
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/server/posts/2016-03-10__the-great-grommet-podcast-episode-two/content.md:
--------------------------------------------------------------------------------
1 | Welcome back to the second installment of the [Great Grommet Podcast](https://blog.grommet.io/post/2016/02/10/welcome-to-the-first-ever-great-grommet-podcast). This week Eric and I had a lot of great topics to discuss. We touched on how Grommet got started and answered one of the most common questions we get, “What makes Grommet so great for the enterprise?”
2 |
3 |
4 |
5 | Here’s an overview of the topics we discussed in this episode:
6 |
7 | * **0:59** — There’s so much out there; why use Grommet?
8 | * **5:15** — The login screen dilemma…The issue that brought Grommet into the world
9 | * **7:30** — Driving an organization and development from a design leadership standpoint
10 | * **10:25** — Having an open-source mindset
11 | * **14:11** — How Grommet is designed for the enterprise
12 | * **19:00** — Contribute to Grommet on Github
13 |
14 | That’s all we have for you this week! Stay tuned for the next episode in a couple weeks and be sure to leave us some comments. Since our podcast is just getting started we’d love your feedback on what you like and what you’d like to hear more about. Bring on the suggestions!
15 |
16 | Lastly, a special thanks to Grommet contributor Yousef for kicking off this week’s podcast.
17 |
--------------------------------------------------------------------------------
/server/posts/2016-03-22__the-great-grommet-podcast-episode-three/content.md:
--------------------------------------------------------------------------------
1 | Eric and Chris are back at it again with the third installment of the Great Grommet Podcast and are joined by special guest and colleague Alan Souza. Alan is celebrating his one-year anniversary with Grommet and gives a previews of his upcoming presentation at the Annual International Technology and Persons with Disabilities Conference on accessibility.
2 |
3 |
4 |
5 | Here’s an overview of the topics discussed in this episode:
6 |
7 | * **0:30** — Alan Souza shares details on his CSUN accessibility presentation
8 | * **6:00** — Rate and Review the Great Grommet Podcast on iTunes!
9 | * **7:40** — Grommet 0.5.4 Release Notes
10 | * **10:00** — Overview of the creation of the Hyper Converged 380 System from Hewlett Packard Enterprise
11 | * **14:00** — Blogging with Grommet
12 |
13 | We have a lot of exciting stuff coming down the pipeline from Grommet so stay tuned for upcoming episodes! As always, any feedback on what you like and what you’d like to hear more about is greatly appreciated. We love your suggestions! Lastly, make sure to [rate and review the Great Grommet Podcast on iTunes](https://itunes.apple.com/us/podcast/great-grommet-podcast/id1089989263).
14 |
15 | A special thanks to Grommet contributor Jackie for doing the intro and to bensound.com for the music.
16 |
--------------------------------------------------------------------------------
/src/js/RouteHistory.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
2 |
3 | import { browserHistory } from 'react-router';
4 | let history = browserHistory;
5 |
6 | // We have our own history implementation so we can insert a prefix.
7 | // This allows sharing web servers and using sub domains with less
8 | // application awareness.
9 |
10 | let appPrefix = '';
11 |
12 | function addPrefix (location) {
13 | let result = location;
14 | if (appPrefix.length > 0) {
15 | if (typeof location === 'string') {
16 | if (location.slice(0, appPrefix.length) !== appPrefix) {
17 | result = (appPrefix + location);
18 | }
19 | } else {
20 | if (location.pathname.slice(0, appPrefix.length) !== appPrefix) {
21 | result = { ...location, pathname: appPrefix + location.pathname };
22 | }
23 | }
24 | }
25 | return result;
26 | }
27 |
28 | function removePrefix (location) {
29 | let result = location;
30 | if (appPrefix.length > 0 &&
31 | location.pathname.slice(0, appPrefix.length) === appPrefix) {
32 | result = { ...location, pathname: location.pathname.slice(appPrefix.length) };
33 | }
34 | return result;
35 | }
36 |
37 | export default {
38 | ...history,
39 | listen: (handler) => {
40 | return history.listen((location) => {
41 | handler(removePrefix(location));
42 | });
43 | },
44 | push: (location) => {
45 | history.push(addPrefix(location));
46 | },
47 | replace: (location) => {
48 | history.replace(addPrefix(location));
49 | },
50 | prefix: (prefix) => {
51 | appPrefix = prefix;
52 | },
53 | makeHref: (path) => `${appPrefix}${path}`
54 | };
55 |
--------------------------------------------------------------------------------
/server/posts/2016-02-10__welcome-to-the-first-ever-great-grommet-podcast/content.md:
--------------------------------------------------------------------------------
1 | We’ve had a blast getting Grommet up and running over the past year, and we hope you’ve enjoyed what we’ve shared on the blog. It’s been fun so far but we’re just getting started!
2 |
3 | Embedded below you can treat your ears to the first-ever Great Grommet podcast. This week’s episode features myself along with the dulcet tones of my Grommet comrade Chris Carlozzi.
4 |
5 |
6 |
7 | Here’s a quick glimpse at some of what we covered:
8 |
9 | * What Grommet is and who we’re building it for (2:39)
10 | * Discussing Ethan Marcotte’s Grommet interview about Responsive Design (7:35)
11 | * Putting yourself in a problem-solving mindset (11:00)
12 | * How people are contributing to Grommet and helping it grow and evolve (16:30)
13 | * Being mobile first in design (18:30)
14 |
15 | We hope to crank out a new episode every couple weeks or so. If we’re lucky enough, we might even convince [Alan](https://twitter.com/alansouzati) to lay down some mad Brazilian beats on his accordion.
16 |
17 | Let us know what you think in the comments and definitely suggest any topics you’d like us to discuss, on Grommet or otherwise, in the future!
18 |
19 | P.S. Thanks to Erhan Abay (a Grommet contributor) for introducing our first podcast and to [Benjamin Tissot](https://twitter.com/Bensound) for our intro and closing music.
--------------------------------------------------------------------------------
/src/js/components/Header.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component } from 'react';
4 | import { Link } from 'react-router';
5 |
6 | import Box from 'grommet/components/Box';
7 | import Anchor from 'grommet/components/Anchor';
8 | import Header from 'grommet/components/Header';
9 | import Heading from 'grommet/components/Heading';
10 | import Title from 'grommet/components/Title';
11 | import GrommetLogo from 'grommet/components/icons/Grommet';
12 |
13 | import history from '../RouteHistory';
14 |
15 | import Search from 'grommet/components/icons/base/Search';
16 | import Archive from 'grommet/components/icons/base/Archive';
17 |
18 | export default class BlogHeader extends Component {
19 |
20 | _onSearch (event) {
21 | event.preventDefault();
22 | history.push('/search');
23 | }
24 |
25 | _onArchive (event) {
26 | event.preventDefault();
27 | history.push('/archive');
28 | }
29 |
30 | render () {
31 |
32 | const logo = (
33 |
34 |
35 |
36 | Blog
37 |
38 |
39 | );
40 |
41 | const search = (
42 | } href="/search" onClick={this._onSearch} />
43 | );
44 |
45 | const archive = (
46 | } href="/archive" onClick={this._onArchive} />
47 | );
48 |
49 | return (
50 |
52 | {logo}
53 |
54 | {search}
55 | {archive}
56 |
57 |
58 | );
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grommet-blog",
3 | "version": "0.1.0",
4 | "main": "src/js/index.js",
5 | "license": "Apache-2.0",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/grommet/grommet-blog.git"
9 | },
10 | "engines": {
11 | "node": ">= 5.0.0",
12 | "npm": ">= 3"
13 | },
14 | "dependencies": {
15 | "babel-plugin-array-includes": "^2.0.3",
16 | "babel-plugin-transform-object-rest-spread": "^6.5.0",
17 | "babel-preset-es2015": "^6.3.13",
18 | "babel-preset-react": "^6.3.13",
19 | "babel-register": "^6.5.2",
20 | "basic-auth-connect": "^1.0.0",
21 | "body-parser": "^1.10.0",
22 | "busboy-body-parser": "0.0.10",
23 | "classnames": "^2.2.3",
24 | "compression": "^1.4.4",
25 | "connect-timeout": "^1.7.0",
26 | "cookie-parser": "^1.3.4",
27 | "del": "^2.2.0",
28 | "ejs": "^2.4.1",
29 | "es6-promise": "^3.2.1",
30 | "express": "^4.10.6",
31 | "express-sslify": "^1.0.1",
32 | "fecha": "^2.0.0",
33 | "fs-extra": "^0.28.0",
34 | "github": "^1.1.2",
35 | "grommet": "https://github.com/grommet/grommet/tarball/stable",
36 | "grommet-templates": "https://github.com/grommet/grommet-templates/tarball/stable",
37 | "highlight.js": "^9.2.0",
38 | "lunr": "^0.6.0",
39 | "mime-types": "^2.1.11",
40 | "morgan": "^1.5.0",
41 | "node-emoji": "^1.3.1",
42 | "node-sass": "^3.4.2",
43 | "react-disqus-thread": "^0.4.0",
44 | "react-router": "^2.0.0",
45 | "rimraf": "^2.5.2",
46 | "simple-git": "^1.27.0"
47 | },
48 | "devDependencies": {
49 | "argv": "0.0.2",
50 | "del": "^2.2.0",
51 | "grommet-toolbox": "^0.1.13",
52 | "gulp": "^3.9.0",
53 | "gulp-git": "^1.7.0",
54 | "mkdirp": "^0.5.1"
55 | },
56 | "scripts": {
57 | "build": "gulp dist",
58 | "start": "node server/server.js"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | parser: babel-eslint
3 |
4 | plugins:
5 | - react
6 |
7 | ecmaFeatures:
8 | jsx: true
9 |
10 | env:
11 | browser: true
12 | node: true
13 |
14 | globals:
15 | __DEV__: true
16 | __THEME__: true
17 | __DEV_MODE__: true
18 | __SOCKET_HOST__: true
19 | IntlPolyfill: true
20 | Modernizr: true
21 | describe: true
22 | it: true
23 | beforeEach: true
24 | afterEach: true
25 | before: true
26 | after: true
27 |
28 | rules:
29 | # ERRORS
30 | space-before-blocks: 2
31 | indent: [2, 2, { SwitchCase: 1 }]
32 | brace-style: 2
33 | comma-dangle: 2
34 | no-unused-expressions: 2
35 | eol-last: 2
36 | dot-notation: 2
37 | consistent-return: 2
38 | no-unused-vars: [2, args: none]
39 | semi: [2, "always"]
40 |
41 | # DISABLED
42 | max-len: 0
43 | #change soon back to max-len: [1, 80]
44 | no-underscore-dangle: 0
45 | new-cap: 0
46 | no-use-before-define: 0
47 | key-spacing: 0
48 | eqeqeq: 0
49 | strict: 0
50 | space-unary-ops: 0
51 | yoda: 0
52 | no-loop-func: 0
53 | no-trailing-spaces: 0
54 | no-multi-spaces: 0
55 | no-shadow: 0
56 | no-alert: 0
57 | no-process-exit: 0
58 | no-extend-native: 0
59 | block-scoped-var: 0
60 | quotes: 0
61 | jsx-quotes: 0
62 |
63 | # REACT DISABLED
64 | react/display-name: 0
65 | react/jsx-sort-prop-types: 0
66 | react/prop-types: 0
67 | react/no-did-mount-set-state: 0
68 | react/no-did-update-set-state: 0
69 | react/jsx-max-props-per-line: 0
70 | react/jsx-sort-props: 0
71 | react/no-multi-comp: 0
72 | react/jsx-boolean-value: 0
73 | react/no-danger: 0
74 |
75 | react/jsx-curly-spacing: 1
76 | react/jsx-no-duplicate-props: 1
77 | react/jsx-no-undef: 1
78 | react/jsx-uses-react: 1
79 | react/jsx-uses-vars: 1
80 | react/no-unknown-property: 1
81 | react/react-in-jsx-scope: 1
82 | react/require-extension: 1
83 | react/self-closing-comp: 1
84 | react/sort-comp: 1
85 | react/wrap-multilines: 1
86 |
--------------------------------------------------------------------------------
/server/posts/2015-11-23__what-is-grommet/content.md:
--------------------------------------------------------------------------------
1 | If you were to give me just one sentence to describe Grommet, I would say it is the most advanced open source UX framework available for enterprise applications. To give a more complete explanation of what Grommet is, I'll step back and explain why we created it.
2 |
3 | Over the years at HP we had customers come to us saying to say various HP products wouldn't integrate well with each other because their UIs were very different, sometimes dramatically so. With this problem in mind, we wanted to create a single offering that would give customers the ability to create a UI with consistent look and feel in their applications that still provided robust UX design capabilities. Our goal was to create something for UX developers that had consumer-grade capabilities with an enterprise user experience framework.
4 |
5 | To address this problem, we took UI code from certain of its products, including the HP OneView IT management software, and packaged it into a reusable library in the form of Grommet. Simultaneously, it published a formal [style guide](https://grommet.github.io/docs/resources) to help developers build applications with consistent UIs.
6 |
7 | Grommet's design is highly modular, allowing developers to use just the parts of Grommet that are most applicable to their application. Users can benefit from using Grommet based on the needs of their users and applications, from referencing the style guide and its basic elements to complete adoption of the platform.
8 |
9 | We feel we've created something special and to back that up, all Hewlett Packard Enterprise software will ship with UIs based on Grommet. We encourage everyone who uses the Grommet open source code – either inside HP or out – to help expand and improve the framework.
10 |
11 | Ready to give Grommet a go? Head on over to our "[Hello World](https://grommet.github.io/docs/hello-world)" guide give it a try using the handy codepen template. For any questions about Grommet as you start tinkering, ping us on our Slack channel at [slackin.grommet.io](http://slackin.grommet.io).
12 |
--------------------------------------------------------------------------------
/server/posts/2015-12-23__hacking-lunchtime-the-usdas-eat-school-lunch-ux-challenge/content.md:
--------------------------------------------------------------------------------
1 | The United States Department of Agriculture (USDA) is looking for a few good UX developers to help kids across the country get better access to school meal programs. The Electronic Application School (EAT) Lunch UX Challenge, hosted on [DevPost](http://devpost.com/), invites UX devs to create the USDA's first-ever model electronic application for households that apply for free or reduced-price school meals.
2 |
3 | Households traditionally apply for these meals through paper or online applications to their respective schools. U.S. government assistance subsidizes all program meals and receives millions of these applications each year. But according to the USDA, the application and approval process is less than smooth:
4 |
5 | > Due to issues with reporting, calculating, and processing, many applications contain errors that result in incorrect eligibility decisions for children.
6 | The Food and Nutrition Service (FNS) offers a prototype paper application on its website, and thousands of school districts have adopted or modified that application for their own use. Many districts also offer online applications, but the FNS does not have an electronic prototype for them to use as a model.
7 |
8 | Developers are tasked with creating a forward-looking, web-based application form using personalized behavioral prompts, UX best practices, and edit-checks to assist in accurate form completion. The ideal application form will help facilitate access to program benefits for eligible children and strengthen program integrity by reducing application errors.
9 |
10 | While warm-fuzzies will abound for anyone who helps tackle this challenge, the USDA is also offering $50,000 in cash prizes, with the best idea taking home a cool $20,000.
11 |
12 | If you’re interested in participating, head over to the EAT School Lunch UX Challenge [page on DevPost](http://lunchux.devpost.com/). Submissions are open now through March 1, 2016.
13 |
14 |
--------------------------------------------------------------------------------
/src/scss/highlight-js.github.scss:
--------------------------------------------------------------------------------
1 | .hljs {
2 | display: block;
3 | overflow-x: auto;
4 | padding: 0.5em;
5 | color: #333;
6 | background: #f8f8f8;
7 | -webkit-text-size-adjust: none;
8 | }
9 |
10 | .hljs-comment,
11 | .diff .hljs-header {
12 | color: #998;
13 | font-style: italic;
14 | }
15 |
16 | .hljs-keyword,
17 | .css .rule .hljs-keyword,
18 | .hljs-winutils,
19 | .nginx .hljs-title,
20 | .hljs-subst,
21 | .hljs-request,
22 | .hljs-status {
23 | color: #333;
24 | font-weight: bold;
25 | }
26 |
27 | .hljs-number,
28 | .hljs-hexcolor,
29 | .ruby .hljs-constant {
30 | color: #008080;
31 | }
32 |
33 | .hljs-string,
34 | .hljs-tag .hljs-value,
35 | .hljs-doctag,
36 | .tex .hljs-formula {
37 | color: #d14;
38 | }
39 |
40 | .hljs-title,
41 | .hljs-id,
42 | .scss .hljs-preprocessor {
43 | color: #900;
44 | font-weight: bold;
45 | }
46 |
47 | .hljs-list .hljs-keyword,
48 | .hljs-subst {
49 | font-weight: normal;
50 | }
51 |
52 | .hljs-class .hljs-title,
53 | .hljs-type,
54 | .vhdl .hljs-literal,
55 | .tex .hljs-command {
56 | color: #458;
57 | font-weight: bold;
58 | }
59 |
60 | .hljs-tag,
61 | .hljs-tag .hljs-title,
62 | .hljs-rule .hljs-property,
63 | .django .hljs-tag .hljs-keyword {
64 | color: #000080;
65 | font-weight: normal;
66 | }
67 |
68 | .hljs-attribute,
69 | .hljs-variable,
70 | .lisp .hljs-body,
71 | .hljs-name {
72 | color: #008080;
73 | }
74 |
75 | .hljs-regexp {
76 | color: #009926;
77 | }
78 |
79 | .hljs-symbol,
80 | .ruby .hljs-symbol .hljs-string,
81 | .lisp .hljs-keyword,
82 | .clojure .hljs-keyword,
83 | .scheme .hljs-keyword,
84 | .tex .hljs-special,
85 | .hljs-prompt {
86 | color: #990073;
87 | }
88 |
89 | .hljs-preprocessor,
90 | .hljs-pragma,
91 | .hljs-pi,
92 | .hljs-doctype,
93 | .hljs-shebang,
94 | .hljs-cdata {
95 | color: #999;
96 | font-weight: bold;
97 | }
98 |
99 | .hljs-deletion {
100 | background: #fdd;
101 | }
102 |
103 | .hljs-addition {
104 | background: #dfd;
105 | }
106 |
107 | .diff .hljs-change {
108 | background: #0086b3;
109 | }
110 |
111 | .hljs-chunk {
112 | color: #aaa;
113 | }
114 |
--------------------------------------------------------------------------------
/server/posts/2015-11-24__welcome-to-our-blog/content.md:
--------------------------------------------------------------------------------
1 | You've stumbled upon the official Grommet blog. Congratulations and welcome.
2 |
3 | I'm Bryan Jacquot, HPE Worldwide User Experience Architect. As part of my job, I am lucky enough to lead the Grommet team.
4 |
5 | Since you’re here, you are probably aware Grommet was officially unveiled back in June 2015. We are really excited about the cool stuff we’ve seen accomplished in the short time Grommet has been available. We are confident it is the most advanced UX framework available for enterprise app designers and developers. We hope to make this blog a place for current and aspiring Grommet users to become better informed about the product they use (or will use) and its capabilities. In an effort to keep you in the know, here is a sampling of the topics we’ll be covering in the weeks and months ahead:
6 |
7 | * **Version Updates**: Never ones to sit on our hands, we're constantly tweaking and improving Grommet to make it an even better experience for our users. In the past week, the Grommet team and community of designers and developers made 62 commits. We mean it when we say we're investing big and moving fast. We'll keep you up to date when we make changes and walk you through what's new and improved.
8 |
9 | * **Grommet in the wild**: As the new kids on the UX block, we're constantly spreading the word about Grommet. We're hitting up Twitter chats, conferences, meetups and everything in between, and we'll give you a peek into our appearances.
10 |
11 | * **How to use Grommet**: Realizing most of you reading this blog are will just be getting your feet wet with Grommet, we'll be posting tips and tricks, as well as how-to guides for getting the most out of your experience.
12 |
13 | * **Team Grommet interviews**: While sharing news you can use about Grommet, we'll also introduce you to the people whose passion for a better UX/UI enterprise experience brought the framework to life.
14 |
15 | * **Designer and developer interviews**: We'll also be looking outside our growing Grommet community to talk with prominent designers and developers who will share how they work and provide helpful insights into what makes them tick.
16 |
17 | We look forward to making this blog as informative and useful as possible and look forward to your feedback. What else would you like to hear? Keep an eye on this space to see what we're up to.
--------------------------------------------------------------------------------
/server/posts/2016-04-18__the-great-grommet-podcast-episode-four-an-interview-with-digital-accessibility-pro-jennison-asuncion/content.md:
--------------------------------------------------------------------------------
1 | In episode 4 of the Great Grommet podcast, Alan Souza and I were joined by digital accessibility pro Jennison Asuncion, who leads accessibility efforts at LinkedIn. We discuss the the overall state of accessibility and Jennison, as a blind user himself, shares tips for making your digital applications more accessible.
2 |
3 |
4 |
5 | Here’s an overview of the topics discussed in this episode:
6 |
7 | * **0:00** — Grommet updates from Chris Carlozzi
8 | * **4:00** — Meet Jennison Asuncion
9 | * **4:53** — An overview of the CSUN Annual International Technology and Persons with Disabilities Conference
10 | * **5:30** — Discussion on issues with navigating the web for a blind user
11 | * **8:00** — Twitter’s new feature for accessibility: alternative access
12 | * **9:00** — Jennison’s background in digital accessibility
13 | * **10:16** — Jennison’s advice for those getting into digital accessibility
14 | * **12:00** — Examples of how Jennison has educated others on accessibility
15 | * **12:28** — Global Accessibility Awareness Day — May 19
16 | * **16:30** — Recommendations for tools to use for testing website accessibility
17 | * **21:03** — Discussing why is it important for enterprise apps to be accessible
18 | * **24:00** — Recommending blogs that talk about accessibility
19 | * **24:50** — Predicting where accessibility will be in 5 to 10 years
20 |
21 | A full list of links mentioned in the podcast, including Twitter handles, accessibility meetups, event opportunities and apps, is located on [the SoundCloud page](https://soundcloud.com/grommetux/episode-4-interview-with-digital-accessibility-pro-jennison-asuncion).
22 |
23 | We’d like to thank Jennison for joining us and hope you’ve enjoyed this newest podcast.
24 |
25 | As always, be sure to leave us feedback and suggestions on what else you’d like to hear about in the comments below.
26 |
27 | Special thanks to [@sogami](https://twitter.com/sogami) for the podcast intro and [Bensound.com](http://www.bensound.com/) for the music.
28 |
--------------------------------------------------------------------------------
/src/js/components/Footer.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component } from 'react';
4 |
5 | import Anchor from 'grommet/components/Anchor';
6 | import Paragraph from 'grommet/components/Paragraph';
7 | import Box from 'grommet/components/Box';
8 | import Footer from 'grommet/components/Footer';
9 | import Menu from 'grommet/components/Menu';
10 | import SocialSlack from 'grommet/components/icons/base/SocialSlack';
11 | import SocialTwitter from 'grommet/components/icons/base/SocialTwitter';
12 | import SocialFacebook from 'grommet/components/icons/base/SocialFacebook';
13 | import SocialVimeo from 'grommet/components/icons/base/SocialVimeo';
14 |
15 | export default class BlogFooter extends Component {
16 | render () {
17 | let socialSlack = (
18 |
20 | );
21 |
22 | let socialTwitter = (
23 |
25 | );
26 |
27 | let socialFacebook = (
28 |
30 | );
31 |
32 | let socialVimeo = (
33 |
34 | );
35 |
36 | return (
37 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/server/posts/2015-12-07__reflections-on-grommet-at-hpe-discover-2015-in-london/content.md:
--------------------------------------------------------------------------------
1 | Last week we spent three days evangelizing Grommet at Hewlett Packard Enterprise (HPE) Discover in London. Given it's only been about six months since Grommet was publicly unveiled, our experience last week was highly encouraging.
2 |
3 | When Grommet was first announced at HP Discover Las Vegas in June 2015, our booth traffic was fairly light. Most people who came by were naturally unfamiliar with the platform and were surprised HP was doing something like Grommet. This time around in London, we talked with a great many more people. There were still some quizzical approaches but there was also much more interest and visible enthusiasm given we’ve been out in the wild for a bit now.
4 |
5 | 
6 |
7 | HPE customers shared with us how Grommet could improve their user's experiences and shorten application development time. Some had limited design resources and were hopeful Grommet would elevate the design of their applications.
8 |
9 | Internal HPE teams also demonstrated an encouraging level of excitement, and there were already a handful of products with Grommet-based demos at the event. We even had people came by just to tell us they or someone on their team had successfully started working with Grommet and were happy with the results. What really surprised us was the level of hopefulness that the entire company would finally be moving toward a more consistent user interface experience. While this has always been an aspect of Grommet, it was encouraging to see it taking shape in this way.
10 |
11 | 
12 |
13 | We came away from the show wanting to provide even more help for current and future Grommet users: example applications and templates; on-boarding paths; and tools to help teams create new themes and provide Grommet documentation to their developers using their own theme. We also want to improve our design resources to help teams get beyond thinking about just re-skinning and instead ask themselves what experiences they could be giving their users. Look for more sample applications coming soon on [grommet.io](https://grommet.github.io)!
14 |
15 | We're already contemplating the next Discover event in Las Vegas in 2016 where we hope to have an expanded presence with an interactive device wall.
16 |
17 | Thanks to everyone who came by and to everyone helping to grow the Grommet community. We came away inspired by what we're all working on and we hope you are too.
18 |
--------------------------------------------------------------------------------
/server/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 | <%- blogTitle %>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 | <%- asyncData %>
33 |
34 |
35 |
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/server/posts/2015-12-10__how-to-contribute-to-grommet-on-github/content.md:
--------------------------------------------------------------------------------
1 | Grommet was built as a fully open-source project and in the spirit of openness, it is currently hosted on GitHub. For those unfamiliar with GitHub, it is the world's largest open-source community and a powerful tool for collaboration.
2 |
3 | We want Grommet to grow and improve with the help of our community. There is a learning curve to get up to speed with the GitHub workflow and we don't want that to be a barrier for you in sharing your valuable contributions. By following the steps in this post, we hope you will understand everything that is required to make your first contribution to Grommet.
4 |
5 | Steps:
6 |
7 | 1. **Create a GitHub account if you don’t have one**: Head to https://github.com/join and fill out the form with your account details.
8 |
9 | 2. **Fork Grommet into your personal account**: Creating a fork is a way to produce a copy of someone else’s project. All your contributions should be done in your local fork. When you feel your contribution is finished, you need to send a Pull Request. Head to https://github.com/grommet/grommet and click the fork button as described in Figure 1.
10 |
11 | 
12 |
13 | 3. **Clone your forked version of Grommet**: Open a terminal window (or use a Git GUI tool of your choice) and run the following command:
14 |
15 | ```bash
16 | git clone https://github.com/${username}/grommet.git
17 | ```
18 |
19 | Replace `${username}` with your Github id.
20 |
21 | 4. **Install Grommet in your local environment**: Read the [Building Grommet wiki page](https://github.com/grommet/grommet/wiki/Building-Grommet) to learn how to install your fork locally.
22 |
23 | 5. **Add a change to your fork**: Now that you have successfully installed Grommet, you can add a change in your local fork and push that to your GitHub. We recommend that you create a Git branch before doing any changes in your local fork. It's a good practice to keep your master branch clean as it makes it easier to keep your local fork always in sync with the main Grommet repository. [Check this blog post](https://gun.io/blog/how-to-github-fork-branch-and-pull-request/) if you want to understand more about forking best practices. The general commands you need to execute are described below:
24 |
25 | ```bash
26 | cd grommet
27 | git checkout -b ${SHORT_CHANGE_TITLE}
28 | git add .
29 | git commit -m "${CHANGE_DESCRIPTION}"
30 | git push origin ${SHORT_CHANGE_TITLE}
31 | ```
32 | Replace `${SHORT_CHANGE_TITLE}` with a clear title of what your change is about. Also, replace `${CHANGE_DESCRIPTION}` with more descriptive information about the contribution.
33 |
34 | 6. **Create a Pull Request**: As soon as your branch is pushed from your local branch to your GitHub fork, you will be able to see a "Compare & Pull Request" button as shown in Figure 2.
35 |
36 | 
37 |
38 | We have provided some [helpful guidelines for contributing to Grommet](https://github.com/grommet/grommet/blob/master/CONTRIBUTING.md), so please make sure to check those out before making a Pull Request.
39 |
40 | 7. **Wait for feedback**: After your Pull Request is submitted, a member of the core Grommet team will review your contribution. If everything looks great, it will be merged with Grommet and your legend will grow.It's possible the Grommet team member reviewing your Pull Request will have feedback and ask that you make some changes. We'll cover that scenario in a separate blog post in the near future. We hope you find the information here useful. Please leave comment if you have any questions!
--------------------------------------------------------------------------------
/src/js/components/Post.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component, PropTypes } from 'react';
4 | import DisqusThread from 'react-disqus-thread';
5 |
6 | import Article from 'grommet/components/Article';
7 | import Box from 'grommet/components/Box';
8 |
9 | import BlogHeader from './Header';
10 | import Footer from './Footer';
11 | import Loading from './Loading';
12 | import Error from './Error';
13 | import PostBody from './PostBody';
14 |
15 | import store from '../store';
16 |
17 | import { setDocumentTitle } from '../utils/blog';
18 |
19 | export default class Post extends Component {
20 |
21 | /**
22 | * used by the server to achieve isomorphic with async data.
23 | * This function must return a promise!
24 | * See /server/blog.js.
25 | */
26 | static fetchData (location, params, appContext) {
27 | store.addContext(appContext);
28 | return store.getPost(params.splat);
29 | }
30 |
31 | constructor () {
32 | super();
33 |
34 | this._onPostReceived = this._onPostReceived.bind(this);
35 | this._onPostFailed = this._onPostFailed.bind(this);
36 |
37 | this.state = {
38 | post: undefined,
39 | loading: true
40 | };
41 | }
42 |
43 | componentDidMount () {
44 | store.getPost(this.props.params.splat).then(
45 | this._onPostReceived, this._onPostFailed
46 | );
47 | }
48 |
49 | componentWillReceiveProps () {
50 | store.getPost(this.props.params.splat).then(
51 | this._onPostReceived, this._onPostFailed
52 | );
53 | }
54 |
55 | _onPostReceived (post) {
56 | if (post) {
57 | setDocumentTitle(post.title);
58 | this.setState({ post: post, loading: false });
59 | } else {
60 | this.setState({ post: post, loading: false });
61 | }
62 | }
63 |
64 | _onPostFailed () {
65 | this.setState({
66 | post: undefined,
67 | loading: false,
68 | error: 'Could not load post. Make sure you have internet connection and try again.'
69 | });
70 | }
71 |
72 | _renderComment () {
73 | return (
74 |
75 |
77 |
78 | );
79 | }
80 |
81 | render () {
82 |
83 | this.post = this.state.post;
84 | this.loading = this.state.loading;
85 | if (store.useContext() && this.context.asyncData) {
86 | this.loading = false;
87 | this.post = this.context.asyncData;
88 | }
89 |
90 | let postNode;
91 | if (this.post) {
92 | postNode = (
93 |
109 |
110 | );
111 | } else {
112 | postNode = (
113 |
114 | );
115 | }
116 |
117 | return (
118 |
119 |
120 | {postNode}
121 |
122 | );
123 | }
124 | };
125 |
126 | Post.propTypes = {
127 | params: PropTypes.object
128 | };
129 |
130 | Post.contextTypes = {
131 | asyncData: PropTypes.any
132 | };
133 |
--------------------------------------------------------------------------------
/server/posts/2016-04-08__top-5-rules-for-giving-enterprise-apps-a-consumer-grade-ux/content.md:
--------------------------------------------------------------------------------
1 | > This article was originally posted on [ProgrammableWeb](http://www.programmableweb.com/news/top-5-rules-giving-enterprise-apps-consumer-grade-ux/analysis/2016/01/26).
2 |
3 | Software users in the consumer market have come to expect what is referred to as the “consumer-grade user experience.” It usually goes something like this: a consumer downloads an app from an app store and if it doesn’t meet their needs, they delete it and move on to the next one. Consumers expect the app to get them started quickly through an inviting and straightforward introductory experience. They do not attend training or read manuals and use applications by choice, not because they are required to do so.
4 |
5 | Generally speaking, the traditional enterprise user experience is the antithesis of the consumer grade user experience. Many enterprise applications were not designed with simplicity, clarity and ease-of-use in mind. Some were not even designed to scale according to the demands of today’s enterprises.
6 |
7 | Enterprise applications often rely on their users to be highly skilled, having undergone extensive training and earning certifications just to use them effectively. There are “quick start” guides that are anything but quick, voluminous release notes and manuals describing every nuance of an application’s expected and unexpected behaviors.
8 |
9 | Users of enterprise applications begrudgingly use them out of duty, not desire. A consumer-grade user experience for the enterprise is the delivery of applications to the enterprise market with enterprise capabilities and a consumer-grade user experience. Such solutions provide a competitive advantage, a strong buying preference and a loyal customer base.
10 |
11 | The ideal consumer-grade user experience needs to have the following five characteristics. Without all of these, it would be difficult for an application to be considered consumer grade:
12 |
13 | * **Simple** – Mobile applications have proven simple designs can be powerful. Consumer-grade experiences present aggregated information and actions necessary for the given task along with appropriate navigation to details. Consumer-grade applications do not overload the user by expecting them to find the salient information among troves of data.
14 |
15 | * **Visually appealing** – This is critical for a consumer-grade user experience. Users are more relaxed, comfortable, confident and capable when using a product that has pleasing aesthetics.
16 |
17 | * **Naturally intuitive** – Users quickly recognize familiar patterns and become immediately successful if they’ve encountered a pattern previously. Enabling users to transfer their knowledge from other applications decreases cognitive load and increases confidence.
18 |
19 | * **Responsive** – User mobility demands applications work on any screen ranging from phone to tablet to desktop to big screen. Responsive applications are composed of responsive components, layouts and patterns which operate naturally on any device at any screen size. Consumer-grade applications do not rely on zooming and panning for readability and navigation.
20 |
21 | * **Accessible** – All users, including those with disabilities, should have a pleasing experience.
22 |
23 | In addition to the above application characteristics, a solution’s user experience extends beyond an application’s user interface. The out-of-box ceremony, physical deployment, installation, registration, upgrade process, security, support and end-of-life cycle are all critically important to delivering a consumer-grade user experience for the enterprise.
24 |
25 | Enterprise users deserve a great experience; the type of experience they’ve now come to expect from applications they use every day outside their jobs. The modern Web, social media, and mobile platforms have changed their expectations.
--------------------------------------------------------------------------------
/server/posts/2016-02-12__why-you-should-be-friends-with-your-style-guide/content.md:
--------------------------------------------------------------------------------
1 | You learn them and you love them. Most designers would say that sarcastically but style guides actually promote creativity rather than stifle it.
2 |
3 | [Grommet’s open source style guide](https://github.com/grommet/grommet-design/raw/master/grommet/grommet-sticker-sheet-apps-general-0-2.sketch), built using [Sketch](http://bohemiancoding.com/sketch/), creates a baseline for designers (and ambidextrous developers) to mold their ideas and applications. By referencing the styles, we not only provide consistency in every app, but we also supply flexibility to create a cohesive brand identity. Some designers may call these guides “constraints,” but Grommet’s guidelines are meant to do just that — guide. If you look closely, you’ll notice each element is crafted to work together seamlessly. That principle is what makes our styles flexible and also results in effortless theming in Grommet.
4 |
5 | Carrie Cousins from [Design Shack](http://designshack.net/articles/graphics/designing-with-constraints-thinking-inside-the-box/) reminds us that thinking inside the box may be a key “to consistency that helps brands establish visual identity and guide voice.” Learning someone else’s design process can profoundly influence your design, regardless of the style.
6 |
7 | Below I’ll walk through some of Grommet’s “keys to consistency” in our open source style guide.
8 |
9 | ### Text Styling
10 |
11 | “Is black text actually black?” “Why do you use dark grey (#333) instead of black (#000) for your Primary text color?” If you look at dark items in nature, there are not many objects that are 100% black. Your shadow, tires on a car, or the night sky, none are pure black. Forcing an app to incorporate too much black can be overwhelming since it’s not “natural” to the human eye and can make it harder to read. \[1\]
12 |
13 | ### Color Palette
14 |
15 | The Grommet purple provides uniqueness amongst its sister frameworks, since we have integrated what is normally a secondary color into our “Call to Action.” Purple gives a great mix between its parent colors, red and blue, by incorporating red’s stimulation and blue’s calm. Purple is said to have “the power to uplift, calm nerves and encourage creativity.” \[2\] This makes purple a universal, all-inclusive color — and it’s not the common-played blue!
16 |
17 | ### Buttons
18 |
19 | Grommet’s buttons have rounded corners to allow for quick visual processing of the button and the text inside. Rounded containers have corners that point inward toward the middle of the button, putting the user’s focus on the text inside. Sharp corners point outward, putting less focus on the inside text and forcing the user to process the information longer.\[3\] Since buttons are fundamental, regularly occurring objects in apps, they need to be easily processed by the user’s eye.
20 |
21 | ### Conclusion
22 |
23 | We hope the Grommet open source style guide will allow you to kick off your next project knowing we have already infused many design principles in the guidelines by default. By building on to our foundation, you can ensure app consistency and the design freedom to focus on what your business needs.
24 |
25 | And remember, you can always [contribute](https://github.com/grommet/grommet-design)! Let us know what you think in the comments.
26 |
27 | ### Footnotes
28 |
29 | 1. “Design Tip: Never Use Black.” Ian Storm Taylor. N.p., n.d. Web. 12 Jan. 2016.
30 | http://ianstormtaylor.com/design-tip-never-use-black/
31 |
32 | 2. “Psychology of the Color Purple and What It Means for Your Business | Fatrabbit CREATIVE.” Website Design | Graphic Design Agency in Morris County NJ | Fatrabbit CREATIVE. N.p., n.d. Web. 12 Jan. 2016.
33 | http://www.fatrabbitcreative.com/blog/psychology_of_the_color_purple_and_what_it_means_for_your_business
34 |
35 | 3. “Why Rounded Corners Are Easier on the Eyes – UX Movement.” UX Movement – Articles on User Experience Design. N.p., n.d. Web. 12 Jan. 2016.
36 | http://uxmovement.com/thinking/why-rounded-corners-are-easier-on-the-eyes/
--------------------------------------------------------------------------------
/.scss-lint.yml:
--------------------------------------------------------------------------------
1 | # Default application configuration that all configurations inherit from.
2 |
3 | scss_files: "**/*.scss"
4 |
5 | linters:
6 | BangFormat:
7 | enabled: true
8 | space_before_bang: true
9 | space_after_bang: false
10 |
11 | BorderZero:
12 | enabled: true
13 | convention: none # or `zero`
14 |
15 | ColorKeyword:
16 | enabled: true
17 |
18 | ColorVariable:
19 | enabled: false
20 |
21 | Comment:
22 | enabled: true
23 |
24 | DebugStatement:
25 | enabled: true
26 |
27 | DeclarationOrder:
28 | enabled: false
29 |
30 | DuplicateProperty:
31 | enabled: true
32 |
33 | ElsePlacement:
34 | enabled: true
35 | style: same_line # or 'new_line'
36 |
37 | EmptyLineBetweenBlocks:
38 | enabled: true
39 | ignore_single_line_blocks: true
40 |
41 | EmptyRule:
42 | enabled: true
43 |
44 | FinalNewline:
45 | enabled: true
46 | present: true
47 |
48 | HexLength:
49 | enabled: false
50 | style: short # or 'long'
51 |
52 | HexNotation:
53 | enabled: false
54 | style: lowercase # or 'uppercase'
55 |
56 | HexValidation:
57 | enabled: true
58 |
59 | IdSelector:
60 | enabled: false
61 |
62 | ImportantRule:
63 | enabled: true
64 |
65 | ImportPath:
66 | enabled: true
67 | leading_underscore: false
68 | filename_extension: false
69 |
70 | Indentation:
71 | enabled: false
72 | allow_non_nested_indentation: false
73 | character: tab # or 'tab'
74 | width: 2
75 |
76 | LeadingZero:
77 | enabled: true
78 | style: include_zero # or 'exclude_zero'
79 |
80 | MergeableSelector:
81 | enabled: true
82 | force_nesting: true
83 |
84 | NameFormat:
85 | enabled: true
86 | allow_leading_underscore: true
87 | convention: hyphenated_lowercase # or 'BEM', or a regex pattern
88 |
89 | NestingDepth:
90 | enabled: false
91 | max_depth: 6
92 |
93 | PlaceholderInExtend:
94 | enabled: true
95 |
96 | PropertyCount:
97 | enabled: false
98 | include_nested: false
99 | max_properties: 10
100 |
101 | PropertySortOrder:
102 | enabled: false
103 |
104 | PropertySpelling:
105 | enabled: true
106 | extra_properties: ['animate']
107 |
108 | QualifyingElement:
109 | enabled: true
110 | allow_element_with_attribute: true
111 | allow_element_with_class: true
112 | allow_element_with_id: false
113 |
114 | SelectorDepth:
115 | enabled: false
116 | max_depth: 4
117 |
118 | SelectorFormat:
119 | enabled: true
120 | convention: hyphenated_BEM # or 'BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern
121 |
122 | Shorthand:
123 | enabled: true
124 |
125 | SingleLinePerProperty:
126 | enabled: true
127 | allow_single_line_rule_sets: true
128 |
129 | SingleLinePerSelector:
130 | enabled: true
131 |
132 | SpaceAfterComma:
133 | enabled: true
134 |
135 | SpaceAfterPropertyColon:
136 | enabled: true
137 | style: at_least_one_space # or 'no_space', or 'on_space', or 'at_least_one_space', or 'aligned'
138 |
139 | SpaceAfterPropertyName:
140 | enabled: true
141 |
142 | SpaceBeforeBrace:
143 | enabled: true
144 | style: space # or 'new_line'
145 | allow_single_line_padding: false
146 |
147 | SpaceBetweenParens:
148 | enabled: true
149 | spaces: 0
150 |
151 | StringQuotes:
152 | enabled: false
153 | style: single_quotes # or double_quotes
154 |
155 | TrailingSemicolon:
156 | enabled: true
157 |
158 | TrailingZero:
159 | enabled: false
160 |
161 | UnnecessaryMantissa:
162 | enabled: true
163 |
164 | UnnecessaryParentReference:
165 | enabled: true
166 |
167 | UrlFormat:
168 | enabled: false
169 |
170 | UrlQuotes:
171 | enabled: false
172 |
173 | VariableForProperty:
174 | enabled: false
175 | properties: []
176 |
177 | VendorPrefix:
178 | enabled: false
179 |
180 | ZeroUnit:
181 | enabled: false
182 |
183 | Compass::*:
184 | enabled: false
185 |
--------------------------------------------------------------------------------
/src/js/Home.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component, PropTypes } from 'react';
4 | import fecha from 'fecha';
5 | import { Link } from 'react-router';
6 |
7 | import Article from 'grommet/components/Article';
8 | import Box from 'grommet/components/Box';
9 | import Heading from 'grommet/components/Heading';
10 | import Section from 'grommet/components/Section';
11 |
12 | import store from './store';
13 | import Header from './components/Header';
14 | import Footer from './components/Footer';
15 | import Loading from './components/Loading';
16 | import Error from './components/Error';
17 |
18 | import { setDocumentTitle } from './utils/blog';
19 |
20 | const HomeSection = (props) => {
21 | return (
22 |
23 |
26 | {props.children}
27 |
28 |
29 | );
30 | };
31 |
32 | export default class Home extends Component {
33 |
34 | /**
35 | * used by the server to achieve isomorphic with async data.
36 | * This function must return a promise!
37 | * See /server/blog.js.
38 | */
39 | static fetchData (location, params, appContext) {
40 | store.addContext(appContext);
41 | return store.getPosts();
42 | }
43 |
44 | constructor () {
45 | super();
46 |
47 | this._onPostsReceived = this._onPostsReceived.bind(this);
48 | this._onPostsFailed = this._onPostsFailed.bind(this);
49 |
50 | this.state = {
51 | posts: [],
52 | loading: true
53 | };
54 | }
55 |
56 | componentDidMount () {
57 | setDocumentTitle();
58 | store.getPosts().then(this._onPostsReceived, this._onPostsFailed);
59 | }
60 |
61 | componentWillReceiveProps () {
62 | store.getPosts().then(this._onPostsReceived, this._onPostsFailed);
63 | }
64 |
65 | _onPostsReceived (posts) {
66 | this.setState({ posts: posts, loading: false });
67 | }
68 |
69 | _onPostsFailed () {
70 | this.setState({
71 | posts: [],
72 | loading: false,
73 | error: 'Could not load posts. Make sure you have internet connection and try again.'
74 | });
75 | }
76 |
77 | render () {
78 |
79 | this.posts = this.state.posts;
80 | this.loading = this.state.loading;
81 | if (store.useContext() && this.context.asyncData) {
82 | this.loading = false;
83 | this.posts = this.context.asyncData;
84 | }
85 |
86 | let postsNode;
87 | let footerNode;
88 | if (this.posts.length > 0) {
89 | footerNode = (
90 |
91 | );
92 | postsNode = this.posts.map((post, index) => {
93 | let formattedDate = fecha.format(
94 | new Date(post.createdAt), 'MMMM D, YYYY'
95 | );
96 |
97 | let backgroundOptions = {};
98 | if (post.coverImage) {
99 | backgroundOptions.backgroundImage = `url(${post.coverImage})`;
100 | } else {
101 | backgroundOptions.colorIndex = 'grey-2';
102 | }
103 |
104 | return (
105 |
107 |
108 | {post.title}
109 |
110 | {`Posted ${formattedDate} by ${post.author}`}
111 |
112 |
113 |
114 | );
115 | });
116 | } else if (this.state.error) {
117 | postsNode = (
118 |
119 | );
120 | } else if (!this.loading) {
121 | postsNode = (
122 |
136 | );
137 | } else {
138 | archiveNode = (
139 |
140 | );
141 | }
142 |
143 | return (
144 |
145 |
146 |
148 | Archive
149 | {archiveNode}
150 |
151 | {footerNode}
152 |
153 | );
154 | }
155 | };
156 |
157 | Archive.propTypes = {
158 | params: PropTypes.object
159 | };
160 |
161 | Archive.contextTypes = {
162 | asyncData: PropTypes.any
163 | };
164 |
--------------------------------------------------------------------------------
/server/posts/2016-03-21__designing-in-a-large-enterprise-company-part-1-design-a-very-overused-term/content.md:
--------------------------------------------------------------------------------
1 | Design is a very inclusive term many application and software companies use to describe something they need: changes to an existing user experience or a new customer experience.
2 |
3 | This article is part of a larger series looking at designers and the design culture found in large organizations. The series will to focus on the application and product design aspect of the term. For now I’ll focus on the designer’s place inside a software-based organization, excluding the hiring of agencies for product branding and marketing.
4 |
5 | As a designer who has spent 17 years working both outside and inside enterprise companies, I hope what I’ve learned will be useful for designers and non-designers alike.
6 |
7 | When in a large company there are many of ways to introduce design to a company. I’ll outline just a few:
8 |
9 | * Creating a strategic structure to create, manage and orchestrate a design group within the entire organization.
10 |
11 | * Hiring designers on a per-product basis. This gives the power to individual business units in structuring and allotting designers/teams that fit their business goals.
12 |
13 | * Hiring externally and using agencies, firms or individuals usually tasked for a specific design need. These engagements can be short- or long-term depending on how symbiotic the relationship becomes.
14 |
15 | * Bringing in individual contractors at any level of the organization. These are usually short-term hires since most companies have time restrictions, but that does not preclude companies from trying to convert them to full-time employees.
16 |
17 | There are a couple places you find design challenges in an organization: One is the introduction of a design language or system into an organization. The other is with the maturity of the design leadership. Will they cascade the new design and its guidelines through the organization? As a designer this is where you have to start reading between the lines. Every organization has its own “issues.” Apple, Google, or any given startup will have their own and it important to be able to interpret those issues in order to see if it’s a good fit for you and your company.
18 |
19 | If you look to define “design” you’ll see the words like intention, plan and goal, among many others. I love that these words all imply design is an evolving concept. Whether it be how a steering wheel is made to provide a certain feel in your hands or the latest fashion trend for tweens, design will continue to be a moving target that will change just has it has always done. Kirby Ferguson has covered the details of [design evolution](http://everythingisaremix.info/watch-the-series/), so I won’t get into that here.
20 |
21 | We’re evolving. Design is evolving. It’s all a plan that is constantly changing. So what the heck does that have to do with you, the experiences I’ve had in my design career and now in the enterprise space? Well as it turns out, a lot.
22 |
23 | Regardless of being in the consumer or enterprise space, companies are folly to one big area. You guessed it: design. This touchy-feely plan no one can measure (well, we [kind of](http://www.howdesign.com/parse/measure-design/) [can](http://www.businessweek.com/innovate/content/oct2009/id2009105_225354.htm)) is what organizations are constantly trying to evolve in hopes of capturing a part of the cultural zeitgeist. Just like you and me, they are in various states of intention at any given level. Each organization has its own emotional state with regard to its issues and how these issues impact its design and culture.
24 |
25 | Application design uses the marrying of functional and creative concepts together to create a unique user experience. Companies that are design-driven should focus on letting design and customer feedback drive the main metrics that drive their experience. Traditional organizations use design as a reactive/supporting tool to help answer questions on the functional design or driven by customer request. Think of it more of a proactive/reactive approach to crafting an experience. To be a design-driven organization, there needs to be some confidence in leadership focused on [CX](http://blog.usabilla.com/ux-vs-cx-which-is-more-important/). This isn’t something that’s always part of an org. And some feel that the investment at the high-level is not warranted, but that’s beginning to change.
26 |
27 | That brings me to where you fit in. Just like the company where you work or where you are applying for a job, you have your own design and emotional evolution happening (whether you realize it or not). Ensuring both you and the company are happy in that current or future union is tricky business. You’re making a lot of assumptions after getting the green light. And just like any interpersonal relationship in life, your relationship with your job is going to have you dealing with a variety of personalities and emotions.
28 |
29 | Design. It’s a plan, and an intention. Everyone is designing something. Our personal lives have some sort of plan that could be entwined with our job plans. Some plans are bigger than others; some are waiting (or hoping) to be big; and others have nothing to do with our nine-to five-job. With all that comes passion, money and attitude. Let’s start digging into how emotional output effects design culture.
30 |
31 | Stay tuned for the next part in the series… Designing in a Large Enterprise Company, Part 2: Your emotional state of design.
--------------------------------------------------------------------------------
/server/posts/2016-06-09__interview-with-lee-byron-react-developer-at-facebook/content.md:
--------------------------------------------------------------------------------
1 | This month we chatted with Lee Byron from Facebook about React and the product infrastructure team he is apart of. In addition to React, he’s worked on open source projects including GraphQL and Immutable.js. He’s also a prolific speaker and you can check out his latest talk from the [Render 2016 conference here](https://vimeo.com/album/3953264/video/166790294 "Render 2016 conference").
2 |
3 |
4 | Source: Twitter.com/leeb
5 |
6 | Lee was kind enough to answer a few questions about React that we sent his way. Take a look at what he had to say below.
7 |
8 | ## What’s your background with React?
9 |
10 | I was part of designing and building the early versions of React, and designed the component lifecycle API. I also contributed to the early versions of ComponentKit, a UI library for iOS based largely on the architecture of React. I've continued to be part of the React community, speaking at conferences and meetups about React and continuing to investigate solving problems related to React like immutable data stores and syncing information from servers.
11 |
12 | ## What do you wish you knew about React before you started using it?
13 |
14 | When I first started using an early version of React, I thought for sure that it would be very slow. Conceptually whenever anything changes, React re-renders the entire view. However what convinced me was getting a better understanding of exactly what it is that React is doing when reconciling a new representation of a view with the old in order to keep the actual UI up to date. It is often described as first creating a new representation of a view and then diffing this with the old representation. In reality, it actually skips straight to computing a diff which allows for some dramatic speed improvements in the common case: nothing changed.
15 |
16 | ## What tips would you give someone who is just now dipping their toes in React?
17 |
18 | I would recommend against using any of the "bootstrap" projects or introducing any other new libraries at the same time. React is often only one part of an application's architecture, and there are a lot of other libraries out there that work nicely with it, like Babel, Redux, hot-loading, and Flow. Often newcomers find a blog post or "bootstrap" project which tries to force way too much of this on them simultaneously. My advice is to first introduce React and nothing more into your tool belt and start building your UI components. Once you have a good understanding of how and why React does what it does, then you can start introducing more tools.
19 |
20 | ## Do you think React was a good choice for Grommet? Why?
21 |
22 | In essence, Grommet is a library of reusable UI components. Components are often difficult to build in HTML-template-based UI libraries or even traditional MVC libraries because the elements of composition are not granular enough to represent small reusable elements. React's essential building block is the Component, which is exactly what you want for reusable UI elements. React was originally designed for exactly this kind of use and was inspired by XHP - an extension to PHP to allow for Component based static UIs - which defined Facebook's UIs including it's reusable UI elements for many years before React was introduced.
23 |
24 | ## What has surprised you about the adoption of React?
25 |
26 | When React was first pitched internally at Facebook, it faced a lot of opposition because of an intuition that it would be slow, and performance is an important topic at Facebook. The project persevered and when it was clear that intuition was misguided, React started to be adopted by many teams at Facebook. When React was initially open sourced in 2012, it faced immediate negative reaction, mostly due to JSX - an optional syntax addition to JavaScript that makes writing component data structures more legible. We figured that React would be relegated to the sidelines in the open source community as most believed that this coupling of business logic and display logic was a bad idea.
27 |
28 | What's been surprising is just how quickly that perspective changed as many started trying out React for themselves. They found that their front-end business logic and their display logic actually benefited from the cohesion React provided.
29 |
30 | We had no idea how popular React would become, and are humbled and thrilled to see React in use on many major websites and now in many major applications via React Native, ComponentKit, and other libraries which share React's architecture.
31 |
32 | ## Where do you see React a few years from now?
33 |
34 | React itself is largely complete and most of it's evolution is in better understanding the underlying operating mechanisms, and generalizing them further. More recently that line of work has allowed React to actually be completely uncoupled from the web browser. You actually need to also include ReactDOM in order to use React in a browser. React can target any kind of retained-mode UI framework including essentially all popular native application UI frameworks including iOS, Android, and Windows. Future work is trying to divorce React from CSS style sheets by providing more functional layout mechanisms (inspired by ComponentKit) and to allow React's operating mechanisms to be more asynchronous to better support environments which support threads (such as iOS and Android) and to ensure that UIs never stutter or freeze, even when doing complex updates.
35 |
--------------------------------------------------------------------------------
/src/js/components/Search.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import React, { Component, PropTypes } from 'react';
4 | import { Link } from 'react-router';
5 | import fecha from 'fecha';
6 |
7 | import Article from 'grommet/components/Article';
8 | import Box from 'grommet/components/Box';
9 | import Section from 'grommet/components/Section';
10 | import Search from 'grommet/components/Search';
11 |
12 | import BlogHeader from './Header';
13 | import Footer from './Footer';
14 | import Loading from './Loading';
15 | import Error from './Error';
16 | import store from '../store';
17 | import history from '../RouteHistory';
18 |
19 | import { setDocumentTitle } from '../utils/blog';
20 |
21 | export default class BlogSearch extends Component {
22 |
23 | /**
24 | * used by the server to achieve isomorphic with async data.
25 | * This function must return a promise!
26 | * See /server/blog.js.
27 | */
28 | static fetchData (location, params, appContext) {
29 | store.addContext(appContext);
30 | return store.search(location.query.q);
31 | }
32 |
33 | constructor () {
34 | super();
35 |
36 | this._onChange = this._onChange.bind(this);
37 | this._renderPosts = this._renderPosts.bind(this);
38 | this._onSearchResultsFailed = this._onSearchResultsFailed.bind(this);
39 | this._onSearchResultsReceived = this._onSearchResultsReceived.bind(this);
40 |
41 | this.state = {
42 | value: '',
43 | posts: undefined,
44 | searching: false
45 | };
46 | }
47 |
48 | componentDidMount () {
49 | setDocumentTitle('Search');
50 | this.refs.search.focus();
51 | if (this.props.location.query.q) {
52 | this.setState({searching: true, value: this.props.location.query.q });
53 | store.search(this.props.location.query.q).then(
54 | this._onSearchResultsReceived, this._onSearchResultsFailed
55 | );
56 | }
57 | }
58 |
59 | _onChange (event) {
60 | const value = event.target.value;
61 |
62 | if (value === '') {
63 | this.props.location.query.q = undefined;
64 | history.replace(this.props.location);
65 | } else {
66 | this.props.location.query.q = value;
67 | history.replace(this.props.location);
68 | }
69 |
70 | if (value.length > 2) {
71 | this.setState({posts: undefined, searching: true, value: value});
72 | store.search(value).then(
73 | this._onSearchResultsReceived, this._onSearchResultsFailed
74 | );
75 | } else {
76 | this.setState({ posts: undefined, searching: false, value: value });
77 | }
78 |
79 | }
80 |
81 | _onSearchResultsReceived (posts) {
82 | this.setState({
83 | posts: posts || [],
84 | value: this.props.location.query.q,
85 | searching: false
86 | });
87 | }
88 |
89 | _onSearchResultsFailed (err) {
90 | this.setState({
91 | posts: [],
92 | searching: false,
93 | error: 'Could not load posts. Make sure you have internet connection and try again.'
94 | });
95 | }
96 |
97 | _renderPosts () {
98 | let posts = this.posts.map((post, index) => {
99 | const createdAtDate = new Date(post.createdAt);
100 | let day = fecha.format(createdAtDate, 'DD');
101 | let month = fecha.format(createdAtDate, 'MM');
102 | let year = fecha.format(createdAtDate, 'YYYY');
103 | let formattedDate = fecha.format(
104 | createdAtDate, 'MMMM D, YYYY'
105 | );
106 | let formattedAuthor = post.author.replace(' ', '').toLowerCase();
107 | return (
108 |
109 |
154 |
155 | );
156 | } else if (this.state.searching) {
157 | postsNode = (
158 |
159 | );
160 | }
161 |
162 | return (
163 |
164 |
165 |
167 |
174 | {postsNode}
175 |
176 | {footerNode}
177 |
178 | );
179 | }
180 | };
181 |
182 | BlogSearch.propTypes = {
183 | asyncData: PropTypes.any
184 | };
185 |
--------------------------------------------------------------------------------
/server/blog.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import basicAuth from 'basic-auth-connect';
4 | import compression from 'compression';
5 | import express from 'express';
6 | import enforce from 'express-sslify';
7 | import http from 'http';
8 | import morgan from 'morgan';
9 | import bodyParser from 'body-parser';
10 | import busboyBodyParser from 'busboy-body-parser';
11 | import path from 'path';
12 | import post from './post';
13 | import React from 'react';
14 | import { renderToString } from 'react-dom/server';
15 | import { match, RouterContext } from 'react-router';
16 | import sass from 'node-sass';
17 | import timeout from 'connect-timeout';
18 |
19 | import routes from '../src/js/routes';
20 | import BlogContext from '../src/js/BlogContext';
21 |
22 | import {
23 | getPostById
24 | } from './utils/post';
25 |
26 | let styles = sass.renderSync({
27 | file: path.resolve(__dirname, '../src/scss/index.scss'),
28 | includePaths: [path.resolve(__dirname, '../node_modules')],
29 | outputStyle: 'compressed'
30 | }).css;
31 |
32 | const PORT = process.env.PORT || 8070;
33 |
34 | const USER = process.env.GH_USER || 'grommet';
35 | const USER_PASSWORD = process.env.USER_PASSWORD || 'admin';
36 | const ENV = process.env.NODE_ENV || 'development';
37 |
38 | const auth = basicAuth(USER, USER_PASSWORD);
39 |
40 | const app = express();
41 | app.use(timeout(120000));
42 | app.set('views', path.resolve(__dirname, 'views'));
43 | app.set('view engine', 'ejs');
44 |
45 | if (ENV === 'production') {
46 | app.use(enforce.HTTPS({ trustProtoHeader: true }));
47 | }
48 | app.use(compression());
49 | app.use(morgan('tiny'));
50 | app.use(bodyParser.json());
51 | app.use(busboyBodyParser());
52 |
53 | function routerProcessor (req, res, next) {
54 | if (/\..*$/.test(req.url)) {
55 | next();
56 | } else if (/^\/\d{4}\/\d{2}\/\d{2}\/.*/.test(req.url)) {
57 | // necessary for the old posts not following our regex structure
58 | const url = req.url
59 | .replace('e-a-t', 'eat')
60 | .replace('0-6-2', '062')
61 | .replace(
62 | 'great-grommet-podcast-episode-1',
63 | 'welcome-to-the-first-ever-great-grommet-podcast'
64 | );
65 | res.redirect(302, `/../post${url}`);
66 | } else {
67 | //comment entire match block for single page app
68 | match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
69 | if (error) {
70 | res.status(500).send(error.message);
71 | } else if (redirectLocation) {
72 | res.redirect(302, redirectLocation.pathname + redirectLocation.search);
73 | } else if (renderProps) {
74 |
75 | function fetchData () {
76 | let components = renderProps.components.filter(
77 | (component) => component !== undefined
78 | );
79 |
80 | let component = (
81 | components[components.length - 1]
82 | );
83 |
84 | if (component.fetchData) {
85 | return (
86 | component.fetchData(
87 | renderProps.location,
88 | renderProps.params,
89 | ENV === 'production' ? "https://blog.grommet.io" :
90 | `localhost:${PORT}`
91 | )
92 | );
93 | }
94 |
95 | return Promise.resolve();
96 | }
97 |
98 | fetchData().then((asyncData)=> {
99 |
100 | let asyncDataNode = '';
101 | if (asyncData) {
102 |
103 | let asyncDataString = JSON.stringify(asyncData);
104 | asyncDataNode = (
105 | ``
106 | );
107 | }
108 |
109 | let component = (
110 |
112 | );
113 | let body = renderToString(component);
114 |
115 | let blogMetadata = {
116 | blogTitle: 'Grommet Blog',
117 | blogImage: '/img/mobile-app-icon.png',
118 | blogPage: '',
119 | blogKeywords: 'react, svg, grommet',
120 | blogDescription: 'The most advanced open source UX framework for enterprise applications',
121 | blogDate: new Date()
122 | };
123 | if (renderProps.params && renderProps.params.splat) {
124 | getPostById(renderProps.params.splat).then((post) => {
125 | blogMetadata.blogTitle = (
126 | `${post.title} | Grommet Blog`
127 | );
128 |
129 | const keywords = post.tags;
130 |
131 | if (post.coverImage) {
132 | blogMetadata.blogImage = post.coverImage;
133 | }
134 |
135 | blogMetadata.blogPage = post.id;
136 | blogMetadata.blogKeywords = keywords;
137 | blogMetadata.blogDescription = (
138 | post.content.replace(/<(?:.|\n)*?>/gm, '').split('\n')[0]
139 | );
140 | blogMetadata.blogDate = post.createdAt;
141 |
142 | res.render('index.ejs', {
143 | appBody: body,
144 | styleContent: styles,
145 | asyncData: asyncDataNode,
146 | ...blogMetadata
147 | });
148 | });
149 | } else {
150 | res.render('index.ejs', {
151 | appBody: body,
152 | styleContent: styles,
153 | asyncData: asyncDataNode,
154 | ...blogMetadata
155 | });
156 | }
157 | }, (err) => {
158 | res.status(500).send(err);
159 | });
160 | } else {
161 | res.status(404).send('Not found');
162 | }
163 | });
164 |
165 | //for single page app use this instead
166 | //res.sendFile(path.resolve(path.join(__dirname, '/../dist/index.html')));
167 | }
168 | }
169 |
170 | app.get('/manage', auth);
171 | app.use('/api/post', post);
172 | app.use('/', routerProcessor);
173 | app.use('/', express.static(path.join(__dirname, '/../dist')));
174 | app.get('/*', routerProcessor);
175 |
176 | const server = http.createServer(app);
177 | server.listen(PORT);
178 |
179 | console.log('Server started, listening at: http://localhost:8070...');
180 |
--------------------------------------------------------------------------------
/server/posts/2015-12-28__2016-responsive-design-trends-with-ethan-marcotte/content.md:
--------------------------------------------------------------------------------
1 | The Grommet team recently connected with designer and author Ethan Marcotte to talk about responsive design trends for 2016. He coined the term “[responsive web design](http://alistapart.com/article/responsive-web-design)” back in 2010 to describe a new way of designing for the ever-changing Web. We took the opportunity to ask Ethan a few questions on how he got started and where he sees responsive web design going in 2016. Enjoy the insights and comment below with any additional questions for Ethan.
2 |
3 | **G: For our audience, it would be great to know a little bit about you, how you became the face of Responsive Web Design (RWD), started writing for A Book Apart, and what you've learned as the web continues to change?**
4 |
5 | **E**: Sure! I'm an independent designer based in Boston, and first coined the term “responsive web design” in a [2010 conference talk](https://vimeo.com/34662135). From there, it turned into [an article](http://alistapart.com/article/responsive-web-design), and then, a year later, [a little yellow book](http://abookapart.com/products/responsive-web-design). Honestly, I wasn’t expecting “responsive design” to last much longer than the original article—I’m still shocked, humbled, and excited by how many people have embraced the approach.
6 |
7 | And as they have, the discussion about responsive design has gotten much broader and nuanced. It’s embraced issues of [design process](http://markboulton.co.uk/journal/how-we-work), of [performance](https://www.filamentgroup.com/lab/responsible-responsive-design.html), of truly [device-agnostic design](http://trentwalton.com/2014/03/10/device-agnostic/)—heck, even [responsive-friendly advertising](http://rogerblack.com/blog/post/the_holy_grail_part_i) has entered the fray. Personally, I think it’s an incredibly exciting time to be a responsive designer; I can’t wait to see what happens next.
8 |
9 | **G: Is there any reason why a UX developer on the web (even in the Enterprise) wouldn’t want to embrace a responsive web experience? And to take that a step further mobile-first?**
10 |
11 | **E**: A colleague of mine once wrote that the web is “[responsive by default](http://blog.andyhume.net/responsive-by-default/)”: in other words, a plain, unstyled web page is natively accessible to every screen and device on the planet. (Heck, you can even try this on the [very first web page](http://info.cern.ch/hypertext/WWW/TheProject.html)).
12 |
13 | That’s not to say that a responsive approach is the answer for every digital project. But it’s an incredibly effective (and affordable) way to design for the Web—and by extension, for all the myriad devices, screens, and contexts that access it.
14 |
15 | But to do responsive design right, it needs to be paired with a “mobile first” mindset. Our audiences—even in Enterprise—are predominantly mobile, making it critical to use those small screens as our starting point. And this is something we’ve heard time and again on our podcast about responsive design: everyone from [Virgin America](http://responsivewebdesign.com/podcast/virgin-america/) to [corporate intranets](http://responsivewebdesign.com/podcast/citrix/) has found that starting with mobile enriches the responsive result. (It makes non-mobile users happier, too.)
16 |
17 | **G: With both web and native experiences having very similar but different approaches to responsive design, is there any advice you would give designers and developers that are trying to work across platforms?**
18 |
19 | **E**: Speaking from my practice, I’ve found it’s best to avoid emulating “native” interfaces on the web. After all, what does “native” mean when your users encounter your UI on countless combinations of browsers and operating systems?
20 |
21 | Instead, try to establish a brand-appropriate aesthetic, and have conversations about how it’ll evolve across different mediums. The BBC’s [Global Experience Language](http://www.bbc.co.uk/gel) is a wonderful example of designing a set of platform-agnostic interface guidelines, put to good use on the [new responsive experience for BBC News](http://www.bbc.com/news).
22 |
23 | **G: You’re pretty prolific, with podcasts, workshops, books, writings on the web, talks, and more, what keeps you driven to do it all and what helps you balance it all?**
24 |
25 | **E**: In short, I’ve found that it’s incredibly energizing to work on challenging projects with good, smart, humane people. And I’ve been incredibly fortunate on that front.
26 |
27 | Over the past year, [Erin Kissane](http://incisive.nu/) edited [my latest book](http://abookapart.com/products/responsive-design-patterns-principles) and [Karen McGrane](http://www.bondartscience.com/) and I continued collaborating on [our podcast](http://responsivewebdesign.com/podcast/citrix/) and [workshops](http://responsivewebdesign.com/workshop/) I worked with some friends and colleagues on a [recent redesign](http://responsivewebdesign.com/toast/) of [The Toast](http://the-toast.net/).
28 |
29 | **G: You’ve no doubt worked within organizations that have many different people delivering experiences that are trying to answer the customer’s needs. With large companies in specific, how do you coalesce the disparate ideas around what tool and process is right for job?**
30 |
31 | **E**: I’ve always found the tools are secondary to the design process. There’s a line I loved from our [Virgin America interview](http://responsivewebdesign.com/podcast/virgin-america/):
32 |
33 | > If you can get something that you can put in a person’s hand and get feedback, that’s the goal. However you get there is how you get there.
34 |
35 | Emphasis mine, but I think it’s true: there’s no singular standard for prototyping, or even a “best” responsive design process. Having a conversation with stakeholders about goals, vision, and broad design objectives will help determine the details, and establish the quickest path to building a prototype.
36 |
37 | **G: Do you consider yourself a developer, a designer, product creator, all, or something altogether different? Does it matter?**
38 |
39 | **E**: Not to me, personally—not really. I love visual design as much as I do front-end development; I thoroughly enjoy writing ([well, mostly](https://twitter.com/beep/status/601352909989801984)), consulting, and product design in equal measure.
40 |
41 | Thanks to Ethan for the time and stay tuned to this blog for more interviews with influencers in responsive design. To read more about Grommet and our mobile design philosophy check out [this page](https://grommet.github.io/docs/mobile).
42 |
43 | The Grommet Team
44 |
45 | Follow [@grommetux](https://twitter.com/grommetux) on Twitter
46 |
47 | Like [Grommet](https://www.facebook.com/grommetux/) on Facebook
48 |
--------------------------------------------------------------------------------
/server/posts/2016-02-04__why-we-chose-react/content.md:
--------------------------------------------------------------------------------
1 | As many of you know, technologists are an opinionated bunch and debates on topics like which JavaScript framework is king are never-ending. With that in mind, the rationale I’m about to share describes why we chose to use React in early 2015 over Angular for building Grommet. I understand I won’t convince everyone. In fact, I’m sure this will cause all sorts of religious reactions. I’m expecting replies countering each point I make with pointers to blogs disproving assertions (yes, I’ve read most of those blogs). The bottom line is we continue to see strong adoption of Grommet based on the React model, and our adopters are providing feedback that affirms the benefits described below.
2 |
3 | With that said, we chose React because of the following benefits. While conducting our investigation, we relied on the experiences and feedback of other adopters. As such, I’ve noted which company pointed out the benefits during our evaluation. Since our adoption, many other companies have started using React.
4 |
5 | * **Declarative**: Probably the biggest benefit we’ve seen with React is the declarative nature that allows developers to declare the structure and behavior of each component. As state changes, it’s handled with events and the code to render the component is written once. This has tremendous benefits in simplifying development and increasing quality. The Virtual DOM is a key enabler of this capability. ([Yahoo!](http://yahooeng.tumblr.com/post/101682875656/evolving-yahoo-mail), [BBC](http://www.bbc.co.uk/blogs/internet/entries/47a96d23-ae04-444e-808f-678e6809765d), [Atlassian HipChat](https://developer.atlassian.com/blog/2015/02/rebuilding-hipchat-with-react/))
6 |
7 | * **Simplicity**: The model is simple; when components are created, they are self-contained and interaction with the components is through a simple interface using properties and states. Once developers start “thinking React” it dramatically simplifies applications. It also makes applications easy to debug because there is a predictable flow of data when data always flows in one direction. ([Atlassian HipChat](https://developer.atlassian.com/blog/2015/02/rebuilding-hipchat-with-react/), [Netflix](http://techblog.netflix.com/2015/01/netflix-likes-react.html))
8 |
9 | * **Isomorphic**: The ability to run the same JavaScript code on both the client and the server allows for both a fast initial page load from the server and a great experience on the client. It’s the best of both worlds and not possible in many web frameworks. ([BBC](http://www.bbc.co.uk/blogs/internet/entries/47a96d23-ae04-444e-808f-678e6809765d))
10 |
11 | * **Small**: It does just enough to develop enterprise applications but isn’t the kitchen sink. With recently added support for ES6 and Components, the API has got even smaller. ( [Netflix](http://techblog.netflix.com/2015/01/netflix-likes-react.html), [Atlassian HipChat](https://developer.atlassian.com/blog/2015/02/rebuilding-hipchat-with-react/))
12 |
13 | * **Fast**: You can read about various performance comparisons but needless to say React is very fast, even faster than some folks who’ve tried to hand-optimize solutions. ([Netflix](http://techblog.netflix.com/2015/01/netflix-likes-react.html))
14 |
15 | * **Testable**: The simple interfaces are easy to test in a headless and automated manner. ([Atlassian HipChat](https://developer.atlassian.com/blog/2015/02/rebuilding-hipchat-with-react/))
16 |
17 | * **Modular**: Using components puts the code (markup and behavior) together in the same module rather than separating technologies (HTML and JavaScript). React takes the modular approach by including everything that’s needed for a module in one file. ([Netflix](http://techblog.netflix.com/2015/01/netflix-likes-react.html))
18 |
19 | * **Short learning curve**: Many have found the learning curve to be dramatically shorter for React than other platforms. The code is simpler and the API can be memorized in a single day. ([Netflix](http://techblog.netflix.com/2015/01/netflix-likes-react.html), [Yahoo!](http://yahooeng.tumblr.com/post/101682875656/evolving-yahoo-mail))
20 |
21 | * **Community**: React is the fastest-growing JavaScript platform on GitHub. The Grommet team frequently attends meet-ups in the San Francisco Bay Area and is closely connected to the React community. ([Yahoo!](http://yahooeng.tumblr.com/post/101682875656/evolving-yahoo-mail))
22 |
23 | While we don’t have the React Native project for Grommet going yet, we’re starting soon. This will allow developers to learn one platform and write applications for the Web, iOS and Android. Compare this to Web, Swift/Objective C and Android/Java.
24 |
25 | Having said all this, Angular was the obvious choice when we started our evaluation. However, as we were starting Grommet in early 2015, Angular 2.0 was the hot topic. The [Angular conference in October 2014](http://angularjs.blogspot.com/2014/10/ng-europe-angular-13-and-beyond.html) talked up Angular 2.0 in a big way. Angular 2.0 was originally targeted for release in October 2015 but still hasn’t released as of January 2016; it’s now said to be “[really close](https://jaxenter.com/angular-2-is-coming-soon-but-angular-1-is-not-going-anywhere-121678.html).” As we evaluated Angular, we realized that the rug was going to be pulled out from under us when 2.0 was released. The Angular community stated publicly that there will be no migration path explored from Angular 1.x to 2.0 until after 2.0 releases. That was a really tough position to put our project and company in when viewed from a macro level. In August of 2015, the Angular team [announced](http://angularjs.blogspot.com/2015/08/angular-1-and-angular-2-coexistence.html) plans to support Angular 1.x and 2.0 in the same application for migration purposes. However, the migration is far from automated.
26 |
27 | Another detracting finding we uncovered in our investigation was the fact that Google itself barely used Angular. At the time of our [evaluation](https://www.quora.com/What-Google-products-make-use-of-AngularJS), Google was using Angular for DoubleClick and YouTube on the PS3…and that was all we could find. In that case, maybe it makes sense why the Angular community is willing to completely change the core of the platform. They don’t have their entire enterprise to migrate down a new path. I couldn’t put Grommet or Hewlett Packard Enterprise in that position.
28 |
29 | In the end there is no perfect platform. Grommet provides a solid foundation that is enabling teams to create great enterprise applications. We are pleased with our decision to adopt React and are excited to continue growing the capabilities of the platform and the community.
30 |
31 | ### References
32 |
33 | * Atlassian HipChat – https://developer.atlassian.com/blog/2015/02/rebuilding-hipchat-with-react/
34 | * Yahoo! Mail – http://yahooeng.tumblr.com/post/101682875656/evolving-yahoo-mail
35 | * Netflix – http://techblog.netflix.com/2015/01/netflix-likes-react.html
36 | * BBC – http://www.bbc.co.uk/blogs/internet/entries/47a96d23-ae04-444e-808f-678e6809765d
37 | * Angular 1 and 2 integration – http://angularjs.blogspot.com/2015/08/angular-1-and-angular-2-coexistence.html
38 | * Angular 2 is coming soon – https://jaxenter.com/angular-2-is-coming-soon-but-angular-1-is-not-going-anywhere-121678.html
39 | * What Google products make use of AngularJS – https://www.quora.com/What-Google-products-make-use-of-AngularJS
--------------------------------------------------------------------------------
/src/js/components/PostBody.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Link } from 'react-router';
3 | import fecha from 'fecha';
4 | import emoji from 'node-emoji';
5 |
6 | import Box from 'grommet/components/Box';
7 | import Heading from 'grommet/components/Heading';
8 | import Footer from 'grommet/components/Footer';
9 | import Tags from 'grommet/components/Tags';
10 | import Tag from 'grommet/components/Tag';
11 | import Markdown from 'grommet/components/Markdown';
12 |
13 | import SocialFacebook from 'grommet/components/icons/base/SocialFacebook';
14 | import SocialTwitter from 'grommet/components/icons/base/SocialTwitter';
15 | import SocialLinkedin from 'grommet/components/icons/base/SocialLinkedin';
16 | import SocialReddit from 'grommet/components/icons/base/SocialReddit';
17 |
18 | import history from '../RouteHistory';
19 |
20 | //hjjs configuration
21 | import hljs from 'highlight.js/lib/highlight';
22 | import bash from 'highlight.js/lib/languages/bash';
23 | import xml from 'highlight.js/lib/languages/xml';
24 | import javascript from 'highlight.js/lib/languages/javascript';
25 | import scss from 'highlight.js/lib/languages/scss';
26 | import css from 'highlight.js/lib/languages/css';
27 |
28 | hljs.registerLanguage('bash', bash);
29 | hljs.registerLanguage('xml', xml);
30 | hljs.registerLanguage('javascript', javascript);
31 | hljs.registerLanguage('scss', scss);
32 | hljs.registerLanguage('css', css);
33 |
34 | function _onSocialClick (event) {
35 | event.preventDefault();
36 | }
37 |
38 | function _onArchiveTag (tag, event) {
39 | event.preventDefault();
40 | history.push(`/archive/tag/${tag}`);
41 | }
42 |
43 | function _renderPostHeader (post, preview) {
44 | const createdAtDate = post.createdAt ?
45 | new Date(post.createdAt): new Date();
46 | let day = fecha.format(createdAtDate, 'DD');
47 | let month = fecha.format(createdAtDate, 'MM');
48 | let year = fecha.format(createdAtDate, 'YYYY');
49 | let formattedDate = fecha.format(
50 | createdAtDate, 'MMMM D, YYYY'
51 | );
52 |
53 | const author = post.author || 'AUTHOR_NAME';
54 | let formattedAuthor = (
55 | author.replace(' ', '').toLowerCase()
56 | );
57 |
58 | let target = (
59 | `http://blog.grommet.io/post/${post.id || ''}`
60 | );
61 |
62 | let backgroundOptions = {};
63 | if (post.coverImage) {
64 | backgroundOptions.backgroundImage = `url(${post.coverImage})`;
65 | } else {
66 | backgroundOptions.colorIndex = 'grey-2';
67 | }
68 |
69 | let secondaryHeader;
70 | if (preview) {
71 | secondaryHeader = (
72 |
73 | Posted {formattedDate} by {author}
74 |
75 | );
76 | } else {
77 | secondaryHeader = (
78 |
79 | Posted
80 | {formattedDate}
81 | by
82 | {author}
83 |
84 |
85 | );
86 | }
87 | return (
88 |
90 |
91 | {post.title || 'POST_TITLE'}
92 |
93 | {secondaryHeader}
94 |
240 | );
241 | }
242 | };
243 |
--------------------------------------------------------------------------------
/server/post.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
2 |
3 | import { Router } from 'express';
4 | import basicAuth from 'basic-auth-connect';
5 | import path from 'path';
6 | import mime from 'mime-types';
7 | import fs from 'fs';
8 | import fecha from 'fecha';
9 | import {
10 | loadPosts,
11 | postsMonthMap,
12 | filterPostsMapByMonth,
13 | buildSearchIndex,
14 | addPost,
15 | editPost,
16 | deletePost,
17 | getAllPosts,
18 | cancelChange,
19 | getPendingPost,
20 | getImageAsBase64
21 | } from './utils/post';
22 |
23 | const router = Router();
24 |
25 | let posts;
26 | let postsByMonth;
27 | let searchIndex;
28 | loadPosts().then((loadedPosts) => {
29 | posts = loadedPosts;
30 | postsByMonth = postsMonthMap(posts);
31 | searchIndex = buildSearchIndex(posts);
32 | });
33 |
34 | function pick(map, match) {
35 | return Object.assign({}, ...(
36 | Object.keys(map).map(
37 | (key) => key.indexOf(match) > -1 ? {[key]: map[key]} : undefined
38 | )
39 | ));
40 | }
41 |
42 | const USER = process.env.GH_USER || 'grommet';
43 | const USER_PASSWORD = process.env.USER_PASSWORD || 'admin';
44 |
45 | const auth = basicAuth(USER, USER_PASSWORD);
46 |
47 | function managePost (req, res, postAction) {
48 | const coverName = req.body.cover;
49 |
50 | const metadata = {
51 | id: req.body.id,
52 | createdAt: req.body.createdAt,
53 | title: req.body.title,
54 | author: req.body.author,
55 | tags: (req.body.tags || '').split(',').map((tag) => tag.trim())
56 | };
57 |
58 | let images = [];
59 |
60 | if (req.body.images) {
61 | images = req.body.images.split(',').map((imageName) => {
62 | let image = {
63 | name: imageName
64 | };
65 |
66 | if (imageName === coverName) {
67 | image.cover = true;
68 | }
69 | return image;
70 | });
71 | }
72 |
73 | if (req.files) {
74 | Object.keys(req.files).forEach((key) => {
75 | let image = req.files[key];
76 | if (image.name === coverName) {
77 | image.cover = true;
78 | }
79 | images.push(image);
80 | });
81 | }
82 |
83 | postAction(req.body.content, metadata, images).then(
84 | () => {
85 | if (process.env.BLOG_PERSISTANCE !== 'github') {
86 | loadPosts().then((loadedPosts) => {
87 | posts = loadedPosts;
88 | postsByMonth = postsMonthMap(posts);
89 | searchIndex = buildSearchIndex(posts);
90 |
91 | res.sendStatus(200);
92 | }, (err) => res.status(500).json({ error: err.toString() }));
93 | } else {
94 | res.sendStatus(200);
95 | }
96 | },
97 | (err) => res.status(500).json({ error: err.toString() })
98 | );
99 | }
100 |
101 | router.post('/', auth, function (req, res) {
102 | managePost(req, res, addPost);
103 | });
104 |
105 | router.put('/', auth, function (req, res) {
106 | managePost(req, res, editPost);
107 | });
108 |
109 | router.post('/cancel/', auth, function (req, res) {
110 | if (process.env.BLOG_PERSISTANCE === 'github') {
111 | const metadata = {
112 | action: req.body.action,
113 | id: req.body.id,
114 | title: req.body.title,
115 | createdAt: req.body.createdAt
116 | };
117 |
118 | cancelChange(metadata).then(
119 | res.sendStatus(200),
120 | (err) => res.status(500).json({ error: err.toString() })
121 | );
122 | } else {
123 | res.status(500).json({
124 | error: 'Cancel change only supported with Github persistance.'
125 | });
126 | }
127 | });
128 |
129 | router.delete('/*', auth, function (req, res) {
130 | deletePost(req.params['0']).then(
131 | () => {
132 | if (process.env.BLOG_PERSISTANCE !== 'github') {
133 | loadPosts().then((loadedPosts) => {
134 | posts = loadedPosts;
135 | postsByMonth = postsMonthMap(posts);
136 | searchIndex = buildSearchIndex(posts);
137 |
138 | res.sendStatus(200);
139 | }, (err) => res.status(500).json({ error: err.toString() }));
140 | } else {
141 | res.sendStatus(200);
142 | }
143 | },
144 | (err) => res.status(500).json({ error: err.toString() })
145 | );
146 | });
147 |
148 | router.get('/', function (req, res) {
149 | res.send(posts);
150 | });
151 |
152 | router.get('/archive/', function (req, res) {
153 | res.send(postsByMonth);
154 | });
155 |
156 | router.get('/manage/', function (req, res) {
157 | if (process.env.BLOG_PERSISTANCE === 'github') {
158 | getAllPosts().then(
159 | (allPosts) => {
160 | allPosts = allPosts.sort((post1, post2) => {
161 | return new Date(post2.createdAt) - new Date(post1.createdAt);
162 | });
163 | res.send(postsMonthMap(allPosts));
164 | },
165 | (err) => res.status(500).json({ error: err.toString() })
166 | );
167 | } else {
168 | res.send(postsByMonth);
169 | }
170 |
171 | });
172 |
173 | router.get('/search/', function (req, res) {
174 | const matches = searchIndex.search(req.query.q).map(
175 | (match) => match.ref
176 | );
177 |
178 | let matchingPosts = [];
179 | if (matches && matches.length > 0) {
180 | matchingPosts = posts.filter((post) => matches.includes(post.id));
181 | }
182 | res.send(matchingPosts);
183 | });
184 |
185 | router.get('/archive/author/:authorName', function (req, res) {
186 | let authorPosts = posts.filter(
187 | (post) => post.author.replace(' ', '').toLowerCase() === req.params.authorName
188 | );
189 |
190 | res.send(postsMonthMap(authorPosts));
191 | });
192 |
193 | router.get('/archive/tag/:tagName', function (req, res) {
194 | let tagPosts = posts.filter(
195 | (post) => post.tags.includes(req.params.tagName)
196 | );
197 |
198 | res.send(postsMonthMap(tagPosts));
199 | });
200 |
201 | router.get('/archive/:year', function (req, res) {
202 | res.send(pick(postsByMonth, req.params.year));
203 | });
204 |
205 | router.get('/archive/:year/:month', function (req, res) {
206 | res.send(filterPostsMapByMonth(
207 | postsByMonth, req.params.year, req.params.month
208 | ));
209 | });
210 |
211 | router.get('/archive/:year/:month/:day', function (req, res) {
212 |
213 | let monthPosts = filterPostsMapByMonth(
214 | postsByMonth, req.params.year, req.params.month
215 | );
216 |
217 | Object.keys(monthPosts).forEach((monthLabel) => {
218 | monthPosts[monthLabel] = monthPosts[monthLabel].filter((post) => {
219 | let day = fecha.format(new Date(post.createdAt), 'DD');
220 | return day === req.params.day;
221 | });
222 |
223 | if (monthPosts[monthLabel].length === 0) {
224 | delete monthPosts[monthLabel];
225 | }
226 | });
227 |
228 | res.send(monthPosts);
229 | });
230 |
231 | router.get('/img/*', function (req, res) {
232 | const imagePath = path.resolve(req.url.split('/img/')[1]);
233 |
234 | if (!fs.existsSync(imagePath) && process.env.BLOG_PERSISTANCE === 'github') {
235 | //if the image does not exist, it is inside a pull request
236 | getImageAsBase64(imagePath).then(
237 | (img) => {
238 | res.writeHead(200, {
239 | 'Content-Type': mime.lookup(imagePath),
240 | 'Content-Length': img.length
241 | });
242 | res.end(img);
243 | },
244 | (err) => res.status(500).json({ error: err.toString() }));
245 | } else {
246 | res.sendFile(imagePath);
247 | }
248 | });
249 |
250 | router.get(/\d{4}/, function (req, res) {
251 | let matchingPost;
252 | const postId = req.url.substring(1).split('?')[0];
253 | if (process.env.BLOG_PERSISTANCE === 'github' && req.query.manage) {
254 | getPendingPost(postId).then((pendingPost) => {
255 | if (pendingPost) {
256 | res.send(pendingPost);
257 | } else {
258 | posts.some((post) => {
259 | if (post.id === postId) {
260 | matchingPost = post;
261 | return true;
262 | }
263 | });
264 |
265 | res.send(matchingPost);
266 | }
267 | }, (err) => res.status(500).json({ error: err.toString() }));
268 | } else {
269 | posts.some((post) => {
270 | if (post.id === postId) {
271 | matchingPost = post;
272 | return true;
273 | }
274 | });
275 | res.send(matchingPost);
276 | }
277 | });
278 |
279 | export default router;
280 |
--------------------------------------------------------------------------------
/server/posts/2016-09-01__how-we-landed-on-jest-snapshot-testing-for-javascript/content.md:
--------------------------------------------------------------------------------
1 | Well, choosing a framework for unit testing in JavaScript today is quite a challenge. With so many options to choose from, sometimes it is hard to make a decision. This article is going to explain the reasoning why we chose Jest as our framework for testing our super awesome React-based UX framework [Grommet](https://github.com/grommet/grommet). :stuck_out_tongue_winking_eye:
2 |
3 | Back in the days our tests looked like this:
4 |
5 | ```javascript
6 | var React = require('react/addons');
7 | var TestUtils = React.addons.TestUtils;
8 | var expect = require('expect');
9 | var assert = require('assert');
10 | var sinon = require('sinon');
11 |
12 | var Anchor = require('../../src/js/components/Anchor');
13 |
14 | var jsdom = require('jsdom-no-contextify').jsdom;
15 | global.document = jsdom('');
16 | global.window = document.defaultView;
17 | global.navigator = {
18 | userAgent: 'node.js'
19 | };
20 |
21 | describe('Grommet Anchor', function() {
22 | it('loads a basic Anchor', function() {
23 | var onAnchorClick = sinon.spy();
24 | var Component = TestUtils.renderIntoDocument(
25 |
26 | Test
27 |
28 | );
29 |
30 | var instance = TestUtils.findRenderedDOMComponentWithClass(
31 | Component, 'anchor'
32 | );
33 |
34 | expect(instance).toExist();
35 | assert.equal(instance.getDOMNode().textContent, 'Test');
36 |
37 | TestUtils.Simulate.click(instance.getDOMNode());
38 | assert(onAnchorClick.calledOnce);
39 |
40 | });
41 |
42 | // trust me, more tests here...
43 | });
44 | ```
45 |
46 | Let me explain what is going on here. We are using React with addons to grab the `TestUtils` (I believe today this is a separate module). For assertions we are using `expect` and `assert` libraries. To mock data and functions we are using `sinon`. The `TestUtils.renderIntoDocument` function expects a virtual DOM to be available in the global context, so we are using `jsdom-no-contextify`. You can use `jsdom` today, but in the past the contextify dependency had issues with [installation](https://github.com/tmpvar/jsdom/issues/378), anyone else? Then we get to the `describe` and `it` functions, which should be familiar to you if you ever used a test runner before. To execute a test you need a runner, you are not even seeing it here but we were using [Mocha](https://mochajs.org). There is something else that you are not seeing here that is also useful to mention, we used [Istanbul](https://github.com/gotwarlost/istanbul) to collect code coverage.
47 |
48 | Let's summarize the dependencies required to run a simple Anchor test: TestUtils, expect, assert, sinon, jsdom, mocha, and instanbul. It took me a while before I was able to run my first test after spending some time learning the required configuration for this to work as expected.
49 |
50 | After this we started adding more component tests and we eventually got to a 80+% coverage. Honestly, I regret not starting with proper tests in the first day, but you know, it happens. It took us a considerable amount of time to configure the assertions right to make sure we were testing a component in a good way. After all, we were not only interested on a good code coverage, but also a good **set of assertions**.
51 |
52 | Then, we started facing performance issues and limitations with the JSDOM library. We then decided to refactor our tests and used [shallow renderer](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) to avoid a virtual DOM. This is how our Anchor test looked until a few weeks ago:
53 |
54 | ```javascript
55 | import {test} from 'tape';
56 | import React from 'react';
57 | import TestUtils from 'react-addons-test-utils';
58 | import sinon from 'sinon';
59 |
60 | test('loads a basic Anchor', (t) => {
61 | t.plan(3);
62 | const onAnchorClick = sinon.spy();
63 | const shallowRenderer = TestUtils.createRenderer();
64 | shallowRenderer.render(React.createElement(Anchor, {
65 | href: 'http://google.com',
66 | onClick: onAnchorClick
67 | }));
68 | const anchorElement = shallowRenderer.getRenderOutput();
69 |
70 | if (anchorElement.props.className.indexOf('anchor') > -1) {
71 | t.pass('Anchor has class');
72 | } else {
73 | t.fail('Anchor does not have anchor class');
74 | }
75 |
76 | t.equal(anchorElement.props.href, 'http://google.com', 'Anchor has test href');
77 |
78 | anchorElement.props.onClick();
79 | t.ok(onAnchorClick.called, 'Anchor click callback was invoked');
80 | });
81 | ```
82 |
83 | So, let's check how we improved here, not to mention that we upgraded the tests to ES6 :tada:. We moved from Mocha to Tape and with that we don't require `assert` and `expect` since Tape has them built-in. We still need sinon, but we don't need JSDOM anymore. `TestUtils` from React provides a `createRenderer` function where you can mount your component in a shallow environment. We still have to create the assertions, but now they are based on the props, which in our opinion is much better then dealing with the DOM nodes.
84 |
85 | We were quite happy with this solution but we started facing issues with code coverage. As we don't have a DOM, the React lifecycle functions were not invoked. It got really hard to adequately exercise our components and get an acceptable level of code coverage.
86 |
87 | So we started the investigation again, we looked into [Ava](https://github.com/avajs/ava) and [Jest](https://facebook.github.io/jest/) as test runners and [Enzyme](https://github.com/airbnb/enzyme) as a test utility. We also looked into JSDOM again and we discovered it no longer requires contextify.
88 |
89 | Jest really got our attention, mainly for the fact that it looks like a one-stop-shop for unit testing. Jest was on version 14 which introduced one feature that was unique and was the selling point for us: [snapshot testing](https://facebook.github.io/jest/blog/2016/07/27/jest-14.html).
90 |
91 | The idea is that you don't need to write the assertions manually, but instead you take a snapshot of a component with a given configuration, like taking a picture. In the future, if anything changes in the snapshot, the test fails. It's up to the developer to inspect the snapshot and decide whether is an expected change or if it is a bug in their code. Jest really got the "zero-configuration" right and it became really easy for us to write a test and get going in literally no time. This is how I would explain Jest snapshot testing in real life:
92 |
93 | 
94 |
95 | As they explain in the Jest 14 release notes, they have limitations with performance and code coverage. But luckily they have a full-time member [Dmitrii Abramov](https://github.com/dmitriiabramov) who refactored a lot of things and my kudos to him for being so responsive with the issues I've faced. Recently they released Jest 15 which meets our needs. This is how our [Anchor test](https://github.com/grommet/grommet/blob/master/__tests__/components/Anchor-test.js) is today:
96 |
97 | ```javascript
98 | import React from 'react';
99 | import renderer from 'react/lib/ReactTestRenderer';
100 |
101 | import Anchor from '../../src/js/components/Anchor';
102 |
103 | describe('Anchor', () => {
104 | it('has correct default options', () => {
105 | const onAnchorClick = jest.fn();
106 | const component = renderer.create(
107 |
108 | );
109 | let tree = component.toJSON();
110 | expect(tree).toMatchSnapshot();
111 |
112 | tree.props.onClick();
113 | tree = component.toJSON();
114 | expect(tree).toMatchSnapshot();
115 | expect(onAnchorClick).toBeCalled();
116 | });
117 |
118 | // trust me, more tests...
119 | });
120 | ```
121 |
122 | Jest really has everything together: a test runner, assertion, and mocking. Here are the benefits we value with using Jest and its Snapshot strategy:
123 |
124 | * **Zero configuration**: the default environment for Jest is jsdom, but all the configuration is managed inside the library. The same goes for coverage, behind the scenes they are using istanbul. They used Jasmine as a test runner in the past, but now they've got their own runner.
125 | * **Less dependencies**: it is really fast to install and learn it. You don't have to spend a lot of time learning and configuring dependencies, it just works.
126 | * **Performance**: it runs tests in parallel to optimize performance. Also, it makes a good use of caching to transform ES6 code, and integrates that well with the coverage report. What you see in the coverage is the ES6 code not the transformed one, again with zero configuration.
127 | * **Fast creation**: we all know that time is always limiting us to write good tests. With snapshots we can get a test running in seconds. Creating assertions manually is time-consuming and we end up not spending enough time writing tests, or worse yet, not writing tests at all.
128 | * **Easier inspection**: the snapshot content is the actual DOM structure. We find it extremely convenient as we can check how our component will render in the browser. We can validate the attached events and make sure the DOM structure is as lean as possible.
129 | * **Great support**: needless to say how responsive the community is. I've faced some issues, reported them, and the maintainers engaged pretty much the same day. In a few days I got all my problems resolved.
130 |
131 | We are excited to be using Jest and we hope this post helps you understand why we chose it. Join our Slack channel and engage with the Grommet community: http://slackin.grommet.io.
132 |
--------------------------------------------------------------------------------
/server/persistance/PostDAO.js:
--------------------------------------------------------------------------------
1 | // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company, L.P.
2 |
3 | import mkdirp from 'mkdirp';
4 | import fecha from 'fecha';
5 | import rimraf from 'rimraf';
6 | import path from 'path';
7 | import fs from 'fs';
8 |
9 | export default class PostDAO {
10 | constructor (postFolderName, content, metadata, images) {
11 | this.postFolderName = postFolderName;
12 | this.content = content;
13 | this.metadata = {...metadata};
14 |
15 | if (images) {
16 | images.some((image) => {
17 | if (image.cover) {
18 | this.metadata.coverImage = image.name;
19 | return true;
20 | }
21 | });
22 | }
23 |
24 | this.images = images || [];
25 |
26 | this.add = this.add.bind(this);
27 | this.edit = this.edit.bind(this);
28 | this.delete = this.delete.bind(this);
29 | this._addMetadata = this._addMetadata.bind(this);
30 | this._addContent = this._addContent.bind(this);
31 | this._addImages = this._addImages.bind(this);
32 | this._editImages = this._editImages.bind(this);
33 | this._deleteImages = this._deleteImages.bind(this);
34 | }
35 |
36 | _addImages () {
37 | return new Promise((resolve, reject) => {
38 | if (this.images.length > 0) {
39 | this.images.forEach((image, index) => {
40 | const imageFolder = path.join(this.postFolder, 'images');
41 | if (!fs.existsSync(imageFolder)) {
42 | fs.mkdirSync(imageFolder);
43 | }
44 | const imageFile = path.join(imageFolder, image.name);
45 | fs.writeFile(
46 | imageFile,
47 | image.data,
48 | 'binary', (err) => {
49 | if (err) {
50 | reject(err);
51 | return false;
52 | } else if (index === this.images.length - 1) {
53 | resolve();
54 | }
55 | }
56 | );
57 | });
58 | } else {
59 | resolve();
60 | }
61 | });
62 | }
63 |
64 | _deleteImages () {
65 | const newImages = this.images.map((image) => image.name);
66 | return new Promise((resolve, reject) => {
67 | const imageFolder = path.join(this.postFolder, 'images');
68 | if (fs.existsSync(imageFolder)) {
69 | let originalImages = fs.readdirSync(imageFolder);
70 | if (!originalImages || originalImages.length === 0) {
71 | resolve();
72 | } else {
73 | originalImages.forEach((image, index) => {
74 | if (newImages.indexOf(image) === -1) {
75 | fs.unlink(path.join(imageFolder, image), (err) => {
76 | if (err) {
77 | reject(err);
78 | } else if (index === originalImages.length - 1) {
79 | resolve();
80 | }
81 | });
82 | } else if (index === originalImages.length - 1) {
83 | resolve();
84 | };
85 | });
86 | }
87 | } else {
88 | resolve();
89 | }
90 | });
91 | }
92 |
93 | _editImages () {
94 | return new Promise((resolve, reject) => {
95 | this._deleteImages().then(() => {
96 | if (this.images.length > 0) {
97 | this.images.forEach((image, index) => {
98 | if (image.data) {
99 | const imageFolder = path.join(this.postFolder, 'images');
100 | if (!fs.existsSync(imageFolder)) {
101 | fs.mkdirSync(imageFolder);
102 | }
103 | const imageFile = path.join(imageFolder, image.name);
104 | fs.writeFile(
105 | imageFile,
106 | image.data,
107 | 'binary', (err) => {
108 | if (err) {
109 | reject(err);
110 | return false;
111 | } else if (index === this.images.length - 1) {
112 | resolve();
113 | }
114 | }
115 | );
116 | } else if (index === this.images.length - 1) {
117 | resolve();
118 | }
119 | });
120 | } else {
121 | resolve();
122 | }
123 | }, reject);
124 | });
125 | }
126 |
127 | _addContent () {
128 | return new Promise((resolve, reject) => {
129 | let contentFile = path.join(this.postFolder, 'content.md');
130 |
131 | fs.writeFile(
132 | contentFile,
133 | this.content,
134 | (err) => {
135 | if (err) {
136 | reject(err);
137 | } else {
138 | resolve();
139 | }
140 | }
141 | );
142 | });
143 | }
144 |
145 | _addMetadata () {
146 | return new Promise((resolve, reject) => {
147 | let metadataFile = path.join(this.postFolder, 'metadata.json');
148 | fs.writeFile(
149 | metadataFile,
150 | JSON.stringify(this.metadata, null, 2),
151 | (err) => {
152 | if (err) {
153 | reject(err);
154 | } else {
155 | resolve();
156 | }
157 | }
158 | );
159 | });
160 | }
161 |
162 | add (root) {
163 | return new Promise((resolve, reject) => {
164 | this.postFolder = path.join(root, `server/posts/${this.postFolderName}`);
165 | mkdirp(this.postFolder, (err) => {
166 | if (err) {
167 | reject(err);
168 | } else {
169 | let promises = [];
170 | promises.push(this._addMetadata());
171 | promises.push(this._addContent());
172 | promises.push(this._addImages());
173 |
174 | Promise.all(promises).then(resolve, reject);
175 | }
176 | });
177 | });
178 | }
179 |
180 | edit (root) {
181 | return new Promise((resolve, reject) => {
182 | this.postFolder = path.join(root, `server/posts/${this.postFolderName}`);
183 |
184 | const createAtDate = new Date(this.metadata.createdAt);
185 | const previousDateFormat = fecha.format(
186 | createAtDate, 'YYYY-MM-DD'
187 | );
188 | const idDateFormat = fecha.format(
189 | createAtDate, 'YYYY/MM/DD'
190 | );
191 |
192 | let titleGroups = this.metadata.id.split('/');
193 | let previousTitleId = titleGroups[titleGroups.length - 1];
194 | const previousFolderName = `${previousDateFormat}__${previousTitleId}`;
195 |
196 | let previousFolder = (
197 | path.join(root, `server/posts/${previousFolderName}`)
198 | );
199 |
200 | let newFolder = (
201 | path.join(root, `server/posts/${this.postFolderName}`)
202 | );
203 |
204 | const titleId = this.metadata.title
205 | .replace(/ /g, '-').replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
206 | this.metadata.id = `${idDateFormat}/${titleId}`;
207 |
208 | if (fs.existsSync(previousFolder)) {
209 | if (previousFolder !== newFolder) {
210 | fs.renameSync(previousFolder, newFolder);
211 | }
212 | this.get(newFolder, 'metadata.json').then((previousMetadata) => {
213 | this.metadata = {...previousMetadata, ...this.metadata};
214 |
215 | let promises = [];
216 | promises.push(this._addMetadata());
217 | promises.push(this._addContent());
218 | promises.push(this._editImages());
219 |
220 | Promise.all(promises).then(resolve, reject);
221 | }, reject);
222 | } else {
223 | reject('Post folder not found.');
224 | }
225 | });
226 | }
227 |
228 | delete (root) {
229 | return new Promise((resolve, reject) => {
230 | const postFolder = path.join(root, `server/posts/${this.postFolderName}`);
231 |
232 | rimraf(postFolder, (err) => {
233 | if (err) {
234 | reject(err);
235 | } else {
236 | resolve();
237 | }
238 | });
239 | });
240 | }
241 |
242 | get (root, file) {
243 | return new Promise((resolve, reject) => {
244 | fs.readFile(
245 | path.join(root, file),
246 | 'utf8',
247 | (err, post) => {
248 | if (err) {
249 | reject(err);
250 | } else {
251 | resolve(JSON.parse(post));
252 | }
253 | }
254 | );
255 | });
256 | }
257 |
258 | getById (id) {
259 | return new Promise((resolve, reject) => {
260 | this.getAll().then((posts) => {
261 | let matchingPost;
262 | posts.some((post) => {
263 | if (post.id === id) {
264 | matchingPost = post;
265 | return true;
266 | }
267 | });
268 |
269 | resolve(matchingPost);
270 | }, reject);
271 | });
272 | }
273 |
274 | getPost (root, postFolder) {
275 | const postRoot = path.join(root, postFolder);
276 | if (fs.lstatSync(postRoot).isDirectory()) {
277 | const imagePrefix = postFolder.startsWith('server/posts') ?
278 | '/api/post/img/' : '/api/post/img/server/posts/';
279 |
280 | const metadataFilename = path.join(postRoot, 'metadata.json');
281 | const contentFilename = path.join(postRoot, 'content.md');
282 |
283 | const metadata = JSON.parse(fs.readFileSync(metadataFilename, 'utf8'));
284 | let content = fs.readFileSync(contentFilename, 'utf8');
285 | const replaceValue = ``;
286 | content = content.replace(/!\[(.*?)\]\((?!http)(.*?)\)/g, replaceValue);
287 |
288 | metadata.tags = metadata.tags.join(', ').trim();
289 | metadata.imagePath = `${imagePrefix}${postFolder}/images`;
290 |
291 | let coverImagePath;
292 | if (metadata.coverImage) {
293 | let imagePath = encodeURIComponent(metadata.coverImage);
294 | coverImagePath = (
295 | `${imagePrefix}${postFolder}/images/${imagePath}`
296 | );
297 | }
298 |
299 | let imageFolder = path.join(postRoot, 'images');
300 | let images = [];
301 | if (fs.existsSync(imageFolder)) {
302 | fs.readdirSync(imageFolder).forEach((image, index) => {
303 | images.push({
304 | id: index,
305 | name: image,
306 | cover: image === metadata.coverImage,
307 | url: (
308 | `${imagePrefix}${postFolder}/images/${image}`
309 | )
310 | });
311 | });
312 | }
313 |
314 | return {
315 | ...metadata,
316 | coverImage: coverImagePath,
317 | content: content,
318 | images: images
319 | };
320 | }
321 | }
322 |
323 | getAll () {
324 | const root = path.resolve(__dirname, '../posts');
325 | return new Promise((resolve, reject) => {
326 | let posts = [];
327 | fs.readdirSync(root).forEach((postFolder) => {
328 | const post = this.getPost(root, postFolder);
329 | if (post) {
330 | posts.push(post);
331 | }
332 | });
333 |
334 | resolve(posts.reverse());
335 | });
336 | }
337 | };
338 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/js/components/manage/Manage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import fecha from 'fecha';
3 |
4 | import Anchor from 'grommet/components/Anchor';
5 | import Article from 'grommet/components/Article';
6 | import Box from 'grommet/components/Box';
7 | import Button from 'grommet/components/Button';
8 | import Header from 'grommet/components/Header';
9 | import Footer from 'grommet/components/Footer';
10 | import Heading from 'grommet/components/Heading';
11 | import Label from 'grommet/components/Label';
12 | import Section from 'grommet/components/Section';
13 | import Tile from 'grommet/components/Tile';
14 | import Tiles from 'grommet/components/Tiles';
15 | import Paragraph from 'grommet/components/Paragraph';
16 |
17 | import EditIcon from 'grommet/components/icons/base/Edit';
18 | import CloseIcon from 'grommet/components/icons/base/Close';
19 | import DeleteIcon from 'grommet/components/icons/base/Trash';
20 | import StatusIcon from 'grommet/components/icons/Status';
21 | import SpinningIcon from 'grommet/components/icons/Spinning';
22 |
23 | import ManageHeader from './Header';
24 | import ManageDeletePost from './ManageDeletePost';
25 | import ManageCancelChangePost from './ManageCancelChangePost';
26 |
27 | import Error from '../Error';
28 | import Loading from '../Loading';
29 | import BlogFooter from '../Footer';
30 | import store from '../../store';
31 | import history from '../../RouteHistory';
32 |
33 | import { setDocumentTitle } from '../../utils/blog';
34 |
35 | export default class Manage extends Component {
36 |
37 | /**
38 | * used by the server to achieve isomorphic with async data.
39 | * This function must return a promise!
40 | * See /server/blog.js.
41 | */
42 | static fetchData (location, params, appContext) {
43 | store.addContext(appContext);
44 | return store.getArchive('/manage');
45 | }
46 |
47 | constructor () {
48 | super();
49 |
50 | this._onArchiveReceived = this._onArchiveReceived.bind(this);
51 | this._onArchiveFailed = this._onArchiveFailed.bind(this);
52 | this._renderArchive = this._renderArchive.bind(this);
53 | this._onRequestToDeletePost = this._onRequestToDeletePost.bind(this);
54 | this._onDeletePost = this._onDeletePost.bind(this);
55 | this._onDeletePostCancel = this._onDeletePostCancel.bind(this);
56 | this._onCancelChangeCancel = this._onCancelChangeCancel.bind(this);
57 | this._onDeleteSucceed = this._onDeleteSucceed.bind(this);
58 | this._onCancelChangeSucceed = this._onCancelChangeSucceed.bind(this);
59 | this._onDeleteFailed = this._onDeleteFailed.bind(this);
60 | this._onCancelChangeFailed = this._onCancelChangeFailed.bind(this);
61 | this._onCancelChange = this._onCancelChange.bind(this);
62 | this._onRequestToCancelChange = this._onRequestToCancelChange.bind(this);
63 | this._onRequestForPost = this._onRequestForPost.bind(this);
64 |
65 | this.state = {
66 | archive: undefined,
67 | loading: true,
68 | delete: false,
69 | deleting: false,
70 | post: undefined
71 | };
72 | }
73 |
74 | componentDidMount () {
75 | setDocumentTitle('Manage');
76 | store.getArchive('/manage').then(
77 | this._onArchiveReceived, this._onArchiveFailed
78 | );
79 | }
80 |
81 | componentWillReceiveProps () {
82 | store.getArchive('/manage').then(
83 | this._onArchiveReceived, this._onArchiveFailed
84 | );
85 | }
86 |
87 | _onArchiveReceived (archive) {
88 | if (archive && Object.keys(archive).length === 0) {
89 | archive = undefined;
90 | }
91 | this.setState({
92 | archive: archive,
93 | loading: false,
94 | deleting: false,
95 | canceling: false,
96 | post: undefined,
97 | error: undefined
98 | });
99 | }
100 |
101 | _onArchiveFailed () {
102 | this.setState({
103 | archive: undefined,
104 | loading: false,
105 | error: 'Could not load posts. Make sure you have internet connection and try again.'
106 | });
107 | }
108 |
109 | _onRequestForPost (event) {
110 | event.preventDefault();
111 | history.push(event.currentTarget.getAttribute('href'));
112 | }
113 |
114 | _onEditPost (postId, event) {
115 | event.preventDefault();
116 | history.push(`/manage/post/edit/${postId}`);
117 | }
118 |
119 | _onDeletePost () {
120 | this.setState({deleting: true, delete: false});
121 | store.deletePost(this.state.post).then(
122 | this._onDeleteSucceed, this._onDeleteFailed
123 | );
124 | }
125 |
126 | _onRequestToDeletePost (post, event) {
127 | event.preventDefault();
128 | this.setState({delete: true, post: post});
129 | }
130 |
131 | _onRequestToCancelChange (post, event) {
132 | event.preventDefault();
133 | this.setState({cancelChange: true, post: post});
134 | }
135 |
136 | _onDeletePostCancel () {
137 | this.setState({delete: false, post: undefined});
138 | }
139 |
140 | _onCancelChangeCancel () {
141 | this.setState({cancelChange: false, post: undefined});
142 | }
143 |
144 | _onDeleteSucceed () {
145 | store.getArchive('/manage').then(
146 | this._onArchiveReceived, this._onArchiveFailed
147 | );
148 | }
149 |
150 | _onDeleteFailed () {
151 | this.setState({
152 | loading: false,
153 | deleting: false,
154 | error: 'Could not delete post. Make sure you have internet connection and try again.'
155 | });
156 | }
157 |
158 | _onCancelChange () {
159 | this.setState({canceling: true, cancelChange: false});
160 | store.cancelChange(this.state.post).then(
161 | this._onCancelChangeSucceed, this._onCancelChangeFailed
162 | );
163 | }
164 |
165 | _onCancelChangeSucceed () {
166 | //giving some time for the github api to reflect the changes.
167 | setTimeout(() => {
168 | store.getArchive('/manage').then(
169 | this._onArchiveReceived, this._onArchiveFailed
170 | );
171 | }, 2000);
172 | }
173 |
174 | _onCancelChangeFailed () {
175 | this.setState({
176 | loading: false,
177 | canceling: false,
178 | error: 'Could not cancel change. Make sure you have internet connection and try again.'
179 | });
180 | }
181 |
182 | _renderArchive () {
183 | let monthKeys = Object.keys(this.archive);
184 | return monthKeys.map((monthLabel, index) => {
185 | const postsByMonth = this.archive[monthLabel];
186 | let posts = postsByMonth.map((post, index) => {
187 | let formattedDate = fecha.format(
188 | new Date(post.createdAt), 'MMMM D, YYYY'
189 | );
190 |
191 | const editIcon = ;
192 |
193 | let footerNode;
194 | if (this.state.deleting && this.state.post.id === post.id) {
195 | footerNode = (
196 |
197 |
198 |
199 |
200 |
201 | Deleting...
202 |
203 |
204 | );
205 | } else {
206 | const deleteIcon = ;
207 | footerNode = (
208 |
209 |
211 |
213 |
214 | );
215 | }
216 |
217 | let colorIndexProp = 'neutral-1';
218 |
219 | let editButton;
220 | if (post.action !== 'Delete') {
221 | editButton = (
222 |
224 | );
225 | }
226 |
227 | let postChangeActions = (
228 |
229 | {editButton}
230 | }
231 | onClick={this._onRequestToCancelChange.bind(this, post)}
232 | a11yTitle={`Cancel ${post.action} ${post.title} post`} />
233 |
234 | );
235 | if (this.state.canceling && this.state.post.id === post.id) {
236 | postChangeActions = (
237 |
238 |
239 |
240 |
241 |
242 | Canceling...
243 |
244 |
245 | );
246 | }
247 |
248 | let headerNode = (
249 |
250 | {post.title}
251 |
252 | );
253 | if (post.pending) {
254 | colorIndexProp = 'grey-2';
255 | footerNode = (
256 |
257 |
258 |
259 |
260 |
261 |
262 | {post.action} post is pending approval
263 |
264 |
265 | {postChangeActions}
266 |
267 | );
268 | } else {
269 | headerNode = (
270 |
271 | {headerNode}
272 |
273 | );
274 | }
275 |
276 | return (
277 |
279 |
280 | {headerNode}
281 |
282 |
283 |
284 | Posted {formattedDate} by {post.author}
285 |
286 |
287 |
290 |
291 | );
292 | }, this);
293 |
294 | return (
295 |
296 |
297 |
298 | {posts}
299 |
300 |
301 | );
302 | });
303 | }
304 |
305 | render () {
306 |
307 | this.archive = this.state.archive;
308 | this.loading = this.state.loading;
309 | if (store.useContext() && this.context.asyncData) {
310 | this.loading = false;
311 | this.archive = this.context.asyncData;
312 | }
313 |
314 | let archiveNode;
315 | let footerNode;
316 | if (this.archive) {
317 | footerNode = (
318 |
319 | );
320 | archiveNode = this._renderArchive();
321 | } else if (!this.loading) {
322 | archiveNode = (
323 |