├── .gitignore ├── README.md ├── _site-config.js ├── content ├── 01-01-2017 │ └── index.md ├── 01-02-2017 │ └── index.md ├── 01-03-2017 │ └── index.md ├── functions │ └── wow │ │ └── index.md ├── tester.md ├── typeography.md └── what.md ├── data ├── examples.json └── tutorials.json ├── functions ├── add-example.js ├── add-tutorial.js ├── package.json └── utils │ ├── createPullRequest.js │ └── sanitize.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── netlify.toml ├── package-lock.json ├── package.json ├── plugins ├── gatsby-better-postcss │ ├── gatsby-node.js │ ├── index.js │ └── package.json └── gatsby-route-plugin │ ├── gatsby-browser.js │ ├── index.js │ └── package.json ├── postcss.config.js ├── src ├── _variables.js ├── analytics.js ├── components │ ├── Button │ │ ├── Button.css │ │ └── index.js │ ├── Card │ │ ├── Card.css │ │ └── index.js │ ├── Copy │ │ ├── Copy.css │ │ └── index.js │ ├── Disqus │ │ └── Disqus.js │ ├── FieldSet │ │ ├── Fieldset.css │ │ └── index.js │ ├── Form │ │ ├── AutoForm.js │ │ └── index.js │ ├── GithubCorner │ │ ├── GithubCorner.css │ │ └── index.js │ ├── Icon │ │ ├── Icon.css │ │ ├── index.js │ │ └── utils │ │ │ └── addSvgToDom.js │ ├── Input │ │ ├── Input.css │ │ ├── index.js │ │ └── utils │ │ │ └── validation.js │ ├── Logo │ │ ├── Logo.css │ │ └── index.js │ ├── Modal │ │ ├── Modal.css │ │ └── index.js │ ├── PostListing │ │ └── PostListing.js │ ├── PostTags │ │ └── PostTags.js │ ├── SEO │ │ └── SEO.js │ ├── SearchBox │ │ ├── SearchBox.css │ │ └── index.js │ └── TextArea │ │ ├── TextArea.css │ │ └── index.js ├── favicon.png ├── fragments │ ├── .gitkeep │ ├── Grid │ │ ├── Grid.css │ │ └── index.js │ └── Sidebar │ │ ├── Sidebar.css │ │ └── index.js ├── icons │ ├── github.svg │ ├── settings.svg │ ├── sprite.js │ └── sprite.svg ├── layouts │ ├── Base │ │ ├── Base.css │ │ └── index.js │ ├── Default │ │ ├── Default.css │ │ └── index.js │ ├── index.css │ └── index.global.css ├── pages │ ├── 404.js │ ├── Home.css │ ├── about.js │ ├── add-example │ │ ├── Add.css │ │ └── index.js │ ├── add-tutorial │ │ ├── Add.css │ │ └── index.js │ ├── admin │ │ ├── Admin.css │ │ ├── examples │ │ │ └── index.js │ │ └── tutorials │ │ │ └── index.js │ ├── directory.js │ ├── examples │ │ ├── Examples.css │ │ └── index.js │ ├── index.js │ └── tutorials │ │ ├── Tutorials.css │ │ └── index.js ├── templates │ ├── Category.js │ ├── Post.css │ ├── Post.js │ └── Tag.js └── utils │ └── data.js └── static ├── favicon.ico ├── logos ├── logo-1024.png └── logo-48.png └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | .DS_Store 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | 52 | # Build Files 53 | misc 54 | public/ 55 | .cache/ 56 | 57 | # Gatsby context 58 | .gatsby-context.js 59 | 60 | # Bundle stats 61 | bundle-stats.json 62 | 63 | # Netlify files 64 | functions-build 65 | 66 | # Local Netlify folder 67 | .netlify -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Netlify Functions 2 |

3 | 4 | Your source for all things Netlify functions. 5 | 6 | - [Function examples](https://functions.netlify.com/examples) 7 | - [Function tutorials](https://functions.netlify.com/tutorials) 8 | 9 | [Adding examples](#-adding-an-example) 10 | 11 | ## Configuration 12 | 13 | Edit `./_site-config.js` 14 | 15 | ## Install 16 | 17 | ``` 18 | npm install 19 | ``` 20 | 21 | ## Running 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Adding an example 28 | 29 | Update the [data/examples.json](./data/examples.json) to add an example. 30 | 31 | Update the [data/tutorials.json](./data/tutorials.json) to add a tutorial. 32 | -------------------------------------------------------------------------------- /_site-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteTitle: 'Netlify Functions', 3 | siteTitleShort: 'Netlify Functions', // Short site title for homescreen (PWA). 4 | siteTitleAlt: 'Netlify Serverless Functions', // Alternative site title for SEO. 5 | siteDescription: 'Your source for all things functions', 6 | siteLogo: '/logos/logo-1024.png', // Logo used for SEO and manifest. 7 | siteUrl: 'https://functions.netlify.com', // Domain of your website without pathPrefix. 8 | pathPrefix: '/', // Prefixes all links. 9 | siteRss: '/rss.xml', // Path to the RSS file. 10 | siteFBAppID: 'xxxxxxx', // FB Application ID for using app insights 11 | googleAnalyticsID: 'UA-xxxxxxx-5', // GA tracking ID. 12 | disqusShortname: 'netlify-functions', // Disqus shortname. 13 | postDefaultCategoryID: 'Tech', // Default category for posts. 14 | dateFromFormat: 'YYYY-MM-DD', // Date format used in the frontmatter. 15 | dateFormat: 'DD/MM/YYYY', // Date format for display. 16 | userName: 'Netlify', 17 | copyright: 'Copyright © 2019. Netlify', // Copyright string for the footer of the website and RSS feed. 18 | themeColor: '#c62828', // Used for setting manifest and progress theme colors. 19 | backgroundColor: '#e0e0e0' // Used for setting manifest background color. 20 | } 21 | -------------------------------------------------------------------------------- /content/01-01-2017/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Post 1" 3 | category: "test3" 4 | date: "01/03/2017" 5 | tags: 6 | - cheese 7 | - other 8 | --- 9 | 10 | # Post One 11 | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 13 | -------------------------------------------------------------------------------- /content/01-02-2017/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Post 2" 3 | category: "test3" 4 | date: "01/03/2017" 5 | tags: 6 | - cheese 7 | - other 8 | --- 9 | 10 | # Post Two 11 | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 13 | -------------------------------------------------------------------------------- /content/01-03-2017/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Post 3" 3 | category: "test3" 4 | date: "01/03/2017" 5 | tags: 6 | - cheese 7 | - other 8 | --- 9 | 10 | # Post three 11 | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 13 | -------------------------------------------------------------------------------- /content/functions/wow/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "look" 3 | cover: "https://unsplash.it/1152/300/?random?BirchintheRoses" 4 | date: "01/03/2017" 5 | category: "tech" 6 | tags: 7 | - tag 8 | --- 9 | # Look 10 | -------------------------------------------------------------------------------- /content/tester.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "tester" 3 | date: "01/03/2017" 4 | category: "tech" 5 | tags: 6 | - tag 7 | --- 8 | 9 | # tester 10 | 11 | hi 12 | -------------------------------------------------------------------------------- /content/typeography.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "type" 3 | date: "01/03/2017" 4 | category: "tech" 5 | tags: 6 | - tag 7 | --- 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 12 | 13 | # Heading 1 14 | 15 | ## Heading 2 16 | 17 | ### Heading 3 18 | 19 | #### Heading 4 20 | 21 | ##### Heading 5 22 | 23 | ##### Heading 6 24 | 25 | # Heading 1 with paragraph 26 | 27 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 28 | 29 | ## Heading 2 with paragraph 30 | 31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 32 | 33 | ### Heading 3 with paragraph 34 | 35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 36 | 37 | #### Heading 4 with paragraph 38 | 39 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 40 | 41 | ##### Heading 5 with paragraph 42 | 43 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 44 | 45 | # Lists 46 | 47 | ## Ordered List 48 | 49 | 1. First ordered list item 50 | 2. Another item 51 | 3. Another item here 52 | 1. child 1 53 | 2. child 2 54 | 3. child 3 55 | 1. grandchild 1 56 | 2. grandchild 2 57 | 3. grandchild 3 58 | 4. Another item 59 | - unordered child 60 | - unordered child 2 61 | 1. ordered grandchild 1 62 | 2. ordered grandchild 2 63 | 3. ordered grandchild 3 64 | - unordered child 3 65 | 66 | ## Unordered List 67 | 68 | - Item 69 | - Another item 70 | - Another item here 71 | - Another item with nested unordered children 72 | - child 1 73 | - child 2 74 | - child 3 75 | - child 4 with children 76 | - grand child 77 | - grand child 2 78 | - Another item with nested ordered children 79 | 1. child 1 80 | 2. child 2 81 | - unordered grandchild 1 82 | - unordered grandchild 2 83 | - unordered grandchild 3 84 | - Last item 85 | 86 | ## Nested List 87 | 88 | * **Bold Text**. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 89 | 90 | 91 | * **Bold text**. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 92 | 93 | 94 | * **Nested list with paragraph**. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 95 | * list item 1 96 | * list item 2 97 | * list item 3 98 | * list item long Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. 99 | 100 | * **Nested ordered list with paragraph**. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. Pellentesque diam orci, sodales in blandit ut, placerat quis felis. Vestibulum at sem massa, in tempus nisi. Vivamus ut fermentum odio. Etiam porttitor faucibus volutpat. Vivamus vitae mi ligula, non hendrerit urna. Suspendisse potenti. Quisque eget massa a massa semper mollis. 101 | 1. list item 1 102 | 2. list item 2 103 | 3. list item 3 104 | 4. list item long Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae mauris arcu, eu pretium nisi. Praesent fringilla ornare ullamcorper. 105 | 106 | # Blockquote 107 | 108 | > Blockquotes are very handy in email to emulate reply text. 109 | > This line is part of the same quote. 110 | 111 | Quote break. 112 | 113 | > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. 114 | 115 | # Table 116 | 117 | | Tables | Are | Cool | 118 | | ------------- |:-------------:| -----:| 119 | | col 3 is | right-aligned | $1600 | 120 | | col 2 is | centered | $12 | 121 | | zebra stripes | are neat | $1 | 122 | 123 | 124 | # Code 125 | 126 | ```javascript 127 | var s = "JavaScript syntax highlighting"; 128 | alert(s); 129 | ``` 130 | 131 | ```python 132 | s = "Python syntax highlighting" 133 | print s 134 | ``` 135 | 136 | ``` 137 | No language indicated, so no syntax highlighting. 138 | But let's throw in a tag. 139 | ``` 140 | 141 | # Embeds 142 | 143 | ## Twitter 144 | 145 |

We’re launching 🚀 AWS AppSync as a new service for preview later today! Here are some of its features! @apatel72001 #reInvent pic.twitter.com/fG9thG6sAa

— AWS re:Invent (@AWSreInvent) November 28, 2017
146 | 147 | 148 | ## Youtube 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /content/what.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "what" 3 | cover: "https://unsplash.it/1152/300/?random?BirchintheRoses" 4 | date: "01/03/2017" 5 | category: "tech" 6 | tags: 7 | - tag 8 | --- 9 | 10 | # What page 11 | 12 | ⊂◉‿◉つ 13 | -------------------------------------------------------------------------------- /data/tutorials.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Netlify Lambda Functions tutorial", 4 | "description": "How to use Netlify Lambda Functions and add dynamic processing to JAMstack sites", 5 | "url": "https://flaviocopes.com/netlify-functions/", 6 | "date": "Dec 28, 2018" 7 | }, 8 | { 9 | "title": "Write and Deploy Your First Serverless Function in 10 Minutes, or Less", 10 | "description": "I heard about Netlify supports lambda functions, and decided to give it a try. Surprisingly, the process was super simple, and my serverless function was up and running within minutes!", 11 | "url": "https://codeburst.io/write-and-deploy-your-first-serverless-function-within-10-minutes-or-less-d7552fcd6550", 12 | "date": "Jan 2, 2019" 13 | }, 14 | { 15 | "title": "Building Serverless CRUD apps with Netlify Functions & FaunaDB", 16 | "description": "This tutorial demonstrates how to build a CRUD backend using Netlify serverless functions and FaunaDB as the datastore.", 17 | "url": "https://www.netlify.com/blog/2018/07/09/building-serverless-crud-apps-with-netlify-functions--faunadb/", 18 | "date": "July 7, 2018" 19 | }, 20 | { 21 | "title": "Build and Deploy a Serverless Function to Netlify", 22 | "description": "Recently, Netlify has become one of the most popular hosts in Web Development. Netlify offers an incredible variety of features like serverless (lambda) functions", 23 | "url": "https://scotch.io/tutorials/build-and-deploy-a-serverless-function-to-netlify", 24 | "date": "October 31, 2018" 25 | }, 26 | { 27 | "title": "Building a Lambda Function with Netlify", 28 | "description": "Process payments via Stripe + serverless functions", 29 | "url": "https://macarthur.me/posts/building-a-lambda-function-with-netlify", 30 | "date": "February 12, 2018" 31 | }, 32 | { 33 | "title": "Forms, Auth and Serverless Functions on Gatsby and Netlify", 34 | "description": "Authenicated functions and Gatsby", 35 | "url": "https://css-tricks.com/forms-auth-and-serverless-functions-on-gatsby-and-netlify/", 36 | "date": "May 31, 2018" 37 | }, 38 | { 39 | "title": "Netlify Lambdas - As simple as possible", 40 | "description": "A minimal approach to successfully deploy an AWS Lambda on Netlify", 41 | "url": "https://luetkemj.github.io/180505/netlify-lambdas-as-simple-as-possible", 42 | "date": "May 5, 2018" 43 | }, 44 | { 45 | "title": "How to submit forms on a static website with VueJS and Netlify functions", 46 | "description": "How you could submit forms, like a contact form, on that serverless architecture", 47 | "url": "https://medium.com/@marcmintel/how-to-submit-forms-on-a-static-website-with-vuejs-and-netlify-functions-b901f40f0627", 48 | "date": "Sep 12, 2018" 49 | }, 50 | { 51 | "title": "Building a Serverless Comment System with Netlify Functions, Storyblok and Vue.js", 52 | "description": "Build a Serverless comment system powered by Netlify Functions and we’ll use the headless CMS Storyblok as a database", 53 | "url": "https://markus.oberlehner.net/blog/building-a-serverless-comment-system-with-netlify-functions-storyblok-and-vue/", 54 | "date": "August 5, 2018" 55 | }, 56 | { 57 | "title": "Deploy a fullstack Apollo app with Netlify", 58 | "description": "How to run your API using Netlify Functions on AWS Lambda", 59 | "url": "https://blog.apollographql.com/deploy-a-fullstack-apollo-app-with-netlify-45a7dfd51b0b", 60 | "date": "Sep 13, 2018" 61 | }, 62 | { 63 | "title": "Telegram Bot on Netlify Functions", 64 | "description": "Building a simple telegram echo bot built using golang and Netlify Functions", 65 | "url": "https://zede.solutions/blog/telegram-bot-on-netlify-functions/", 66 | "date": "April 7, 2018" 67 | }, 68 | { 69 | "title": "Get Data from MongoDB with Netlify Functions Lambda", 70 | "description": "Learn how to query a database instance and return data without a backend server", 71 | "url": "https://www.edwardbeazer.com/get-data-from-mongodb-with-netlify-functions-lambda/", 72 | "date": "July 31, 2018" 73 | }, 74 | { 75 | "title": "Express.js on Netlify", 76 | "description": "How to run express via Netlify functions", 77 | "url": "https://blog.neverendingqs.com/2018/09/08/expressjs-on-netlify.html", 78 | "date": "Sep 8, 2018" 79 | }, 80 | { 81 | "title": "Sending WebSub notifications from static sites using Netlify functions", 82 | "description": "How to send notifications through protocols like WebSub with serverless functions", 83 | "url": "https://matienzo.org/2018/websub-static/", 84 | "date": "February 13, 2018" 85 | }, 86 | { 87 | "title": "Making the static dynamic: Instagram Importer", 88 | "description": "Automatically rebuild static sites when a new instagram post is found", 89 | "url": "https://www.trysmudford.com/blog/making-the-static-dynamic-instagram-importer/", 90 | "date": "February 12, 2018" 91 | }, 92 | { 93 | "title": "Dynamic Static Sites with Netlify Functions & iOS Shortcuts", 94 | "description": "Share data from iOS Shortcuts to my Netlify site", 95 | "url": "https://bryanlrobinson.com/blog/2018/11/12/ios-shortcuts-pushing-data-to-netlify-static-site/", 96 | "date": "Nov 12, 2018" 97 | }, 98 | { 99 | "title": "Syndicating Content with Netlify functions", 100 | "description": "How to automatically publish content from a static site on Twitter, using Eleventy and Netlify's lambda functions", 101 | "url": "https://mxb.at/blog/syndicating-content-to-twitter-with-netlify-functions/", 102 | "date": "Jan 06, 2019" 103 | }, 104 | { 105 | "title": "Adding Emotional Tone Analysis to Your Contact Form", 106 | "description": "How to use IBM Watson Tone Analyzer & Netlify functions for sentiment analysis on forms", 107 | "url": "https://www.raymondcamden.com/2019/01/18/adding-emotional-tone-analysis-to-your-contact-form", 108 | "date": "Jan 18, 2019" 109 | }, 110 | { 111 | "title": "Building a Serverless JAMStack app with FaunaDB Cloud: Part 1", 112 | "description": "How to use FaunaDB & Netlify functions & identity for CRUD applications", 113 | "url": "https://fauna.com/blog/building-a-serverless-jamstack-app-with-faunadb-cloud-part-1", 114 | "date": "Jan 23, 2019" 115 | }, 116 | { 117 | "title": "Debugging Netlify Function Errors with Sentry", 118 | "description": "How to set up, debug and monitor Netlify Functions with Sentry.io", 119 | "url": "https://httptoolkit.tech/blog/netlify-function-error-reporting-with-sentry/", 120 | "date": "Jan 23, 2019" 121 | }, 122 | { 123 | "title": "How to send text messages from your static site using Netlify, Twilio and serverless functions", 124 | "description": "In this article, I want to share how you can create and deploy a scalable static site on Netlify and how you can use serverless functions to send text messages using Twilio.", 125 | "url": "https://dev.to/twilio/how-to-send-text-messages-from-your-static-site-using-netlify-twilio-and-serverless-functions-24ci/", 126 | "date": "April 01, 2019" 127 | }, 128 | { 129 | "title": "Circumventing CORS with Netlify Functions & Nodejs", 130 | "description": "I needed to consume a third-party API that hadn’t been properly configured with the appropriate CORS headers. Rather than track down the API author, I decided to take this as an opportunity to experiment with a serverless pattern", 131 | "url": "https://medium.com/@kamry.bowman/circumventing-cors-with-netlify-functions-nodejs-65aa6ec69a65", 132 | "date": "April 19, 2019" 133 | }, 134 | { 135 | "title": "CSSTricks: Netlify Functions for Sending Emails", 136 | "description": "Let's say you're rocking a JAMstack-style site (no server-side languages in use), but you want to do something rather dynamic like send an email. Not a problem! That's the whole point of JAMstack.", 137 | "url": "https://css-tricks.com/netlify-functions-for-sending-emails/", 138 | "date": "April 23, 2019" 139 | }, 140 | { 141 | "title": "Handling Static Forms, Auth & Serverless Functions with Gatsby on Netlify", 142 | "description": "Supercharge your static site with forms, password-protected authentication, and AWS Lambda functions.", 143 | "url": "https://snipcart.com/blog/static-forms-serverless-gatsby-netlify", 144 | "date": "April 25, 2019" 145 | }, 146 | { 147 | "title": "How to use Netlify Functions in Elm", 148 | "description": "This worked example creates a simple Netlify Function and integrates it with an Elm application.", 149 | "url": "https://www.freecodecamp.org/news/how-to-use-netlify-functions-in-elm/", 150 | "date": "August 28, 2019" 151 | }, 152 | { 153 | "title": "Netlify Dev + Serverless Functions + MailChimp Subscribe Form Tutorial", 154 | "description": "Create a working serverless function hosted on Netlify, automatically built and deployed every time you push to git, that you can use on your static site to add subscriber emails directly to MailChimp", 155 | "url": "https://medium.com/@mattdgregg/netlify-dev-serverless-functions-mailchimp-subscribe-form-tutorial-28ffaa51ba99", 156 | "date": "September 10, 2019" 157 | }, 158 | { 159 | "title": "Serverless Mailing Lists - Unlimited Email Sign-Ups for Free", 160 | "description": "Create a simple serverless function to collect email from your users for free without external tools", 161 | "url": "https://medium.com/better-programming/unlimited-email-signups-for-free-for-indie-hackers-403c20261880", 162 | "date": "November 6, 2019" 163 | }, 164 | { 165 | "title": "Super simple start to serverless", 166 | "description": "Easily create server code without worrying about managing servers yourself", 167 | "url": "https://kentcdodds.com/blog/super-simple-start-to-serverless", 168 | "date": "December 7, 2019" 169 | }, 170 | { 171 | "title": "Submitting sitemaps on deploy with Netlify", 172 | "url": "https://atymic.dev/tips/netlify-submit-sitemaps/", 173 | "date": "2020-01-14", 174 | "description": "There's plenty of static site generators out there, and most support generating sitemaps. To make sure google indexes your pages as quickly as possible, it's a good idea to ping them with your sitemap when it's updated.\n\nNetlify provides some hooks which can be used to run functions once the deploy is complete. This allows us to ping google when production is deployed." 175 | } 176 | ] -------------------------------------------------------------------------------- /functions/add-example.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const gitUrlParse = require('git-url-parse') 3 | const createPullRequest = require('./utils/createPullRequest') 4 | const sanitize = require('./utils/sanitize') 5 | const Octokit = require('@octokit/rest').plugin(createPullRequest) 6 | 7 | const { REPOSITORY_URL } = process.env 8 | 9 | const octokit = new Octokit() 10 | octokit.authenticate({ 11 | type: 'oauth', 12 | token: process.env.GITHUB_TOKEN 13 | }) 14 | 15 | const FILE_TO_CHANGE = 'data/examples.json' 16 | 17 | /* export our lambda function as named "handler" export */ 18 | exports.handler = async (event, context) => { 19 | const parsed = gitUrlParse('https://github.com/netlify-labs/functions-site') 20 | const repo = parsed.name 21 | const owner = parsed.owner 22 | const body = JSON.parse(event.body) 23 | 24 | if (!repo || !owner) { 25 | return { 26 | statusCode: 401, 27 | body: JSON.stringify({ 28 | data: 'process.env.REPOSITORY_URL malformed' 29 | }) 30 | } 31 | } 32 | 33 | if (!body || !body.name) { 34 | return { 35 | statusCode: 401, 36 | body: JSON.stringify({ 37 | data: 'Request malformed' 38 | }) 39 | } 40 | } 41 | 42 | const cleanName = sanitize(body.name) 43 | console.log('cleanName', cleanName) 44 | if (!cleanName) { 45 | return { 46 | statusCode: 401, 47 | body: JSON.stringify({ 48 | data: 'Request malformed. Bad data has been passed in' 49 | }) 50 | } 51 | } 52 | 53 | // Get repo file contents 54 | let result 55 | try { 56 | result = await octokit.repos.getContents({ 57 | owner, 58 | repo, 59 | path: FILE_TO_CHANGE 60 | }) 61 | } catch (err) { 62 | console.log('octokit.repos.getContents err', err) 63 | return { 64 | statusCode: 400, 65 | body: JSON.stringify({ 66 | error: `${err.message}` 67 | }) 68 | } 69 | } 70 | 71 | if (typeof result.data === 'undefined') { 72 | // createFile(octokit, config, file, content) 73 | // throw file doesnt exist 74 | return { 75 | statusCode: 400, 76 | body: JSON.stringify({ 77 | error: `No ${FILE_TO_CHANGE} found` 78 | }) 79 | } 80 | } 81 | 82 | // content will be base64 encoded 83 | const content = Buffer.from(result.data.content, 'base64').toString() 84 | 85 | const allData = parseFile(FILE_TO_CHANGE, content) 86 | console.log('allData.length', allData.length) 87 | 88 | if (alreadyHasUri(body, allData)) { 89 | console.log(`${body.url} already is in the list!`) 90 | return { 91 | statusCode: 422, 92 | body: JSON.stringify({ 93 | message: `${body.url} already is in the list!` 94 | }) 95 | } 96 | } 97 | 98 | const newData = allData.concat(body) 99 | const newContent = JSON.stringify(newData, null, 2) 100 | 101 | let response = {} 102 | try { 103 | response = await octokit.createPullRequest({ 104 | owner, 105 | repo, 106 | title: `Add example ${body.url}`, 107 | body: `Add ${body.name} at ${body.url}`, 108 | base: 'master', /* optional: defaults to default branch */ 109 | head: `pull-request-branch-name-${new Date().getTime()}`, 110 | changes: { 111 | files: { 112 | [`${FILE_TO_CHANGE}`]: newContent, 113 | }, 114 | commit: `updating ${FILE_TO_CHANGE}` 115 | } 116 | }) 117 | } catch (err) { 118 | if (err.status === 422) { 119 | console.log('BRANCH ALREADY EXISTS!') 120 | return { 121 | statusCode: 400, 122 | body: JSON.stringify({ 123 | error: `BRANCH ALREADY EXISTS!` 124 | }) 125 | } 126 | } 127 | } 128 | console.log('data', response.data) 129 | return { 130 | statusCode: 200, 131 | body: JSON.stringify({ 132 | message: `pr created!`, 133 | url: response.data.html_url 134 | }) 135 | } 136 | } 137 | 138 | /** 139 | * Check if array already has URL 140 | * @return {Boolean} 141 | */ 142 | function alreadyHasUri(newItem, allData) { 143 | return allData.some((item) => { 144 | if (!item.url || !newItem.url) { 145 | return false 146 | } 147 | return niceUrl(item.url) === niceUrl(newItem.url) 148 | }) 149 | } 150 | 151 | function niceUrl(href) { 152 | const { host, pathname } = url.parse(href) 153 | return `${host}${pathname}` 154 | } 155 | 156 | /** 157 | * Stringify to JSON maybe. 158 | * 159 | * @param {string} file 160 | * @param {string} content 161 | * 162 | * @return {string} 163 | */ 164 | function parseFile(file, content) { 165 | if (file.indexOf('.json') !== -1) { 166 | return JSON.parse(content) 167 | } 168 | 169 | return content 170 | } 171 | -------------------------------------------------------------------------------- /functions/add-tutorial.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const gitUrlParse = require('git-url-parse') 3 | const createPullRequest = require('./utils/createPullRequest') 4 | const sanitizeTitle = require('./utils/sanitize') 5 | const Octokit = require('@octokit/rest').plugin(createPullRequest) 6 | 7 | const { REPOSITORY_URL } = process.env 8 | 9 | const octokit = new Octokit() 10 | octokit.authenticate({ 11 | type: 'oauth', 12 | token: process.env.GITHUB_TOKEN 13 | }) 14 | 15 | const FILE_TO_CHANGE = 'data/tutorials.json' 16 | 17 | /* export our lambda function as named "handler" export */ 18 | exports.handler = async (event, context) => { 19 | const parsed = gitUrlParse('https://github.com/netlify-labs/functions-site') 20 | const repo = parsed.name 21 | const owner = parsed.owner 22 | const body = JSON.parse(event.body) 23 | 24 | if (!repo || !owner) { 25 | return { 26 | statusCode: 401, 27 | body: JSON.stringify({ 28 | data: 'process.env.REPOSITORY_URL malformed' 29 | }) 30 | } 31 | } 32 | 33 | if (!body || !body.title) { 34 | return { 35 | statusCode: 401, 36 | body: JSON.stringify({ 37 | data: 'Request malformed. Missing Title' 38 | }) 39 | } 40 | } 41 | 42 | const cleanTitle = sanitizeTitle(body.title) 43 | console.log('cleanTitle', cleanTitle) 44 | if (!cleanTitle) { 45 | return { 46 | statusCode: 401, 47 | body: JSON.stringify({ 48 | data: 'Request malformed. Bad data has been passed in' 49 | }) 50 | } 51 | } 52 | 53 | // Get repo file contents 54 | let result 55 | try { 56 | result = await octokit.repos.getContents({ 57 | owner, 58 | repo, 59 | path: FILE_TO_CHANGE 60 | }) 61 | } catch (err) { 62 | console.log('octokit.repos.getContents err', err) 63 | return { 64 | statusCode: 400, 65 | body: JSON.stringify({ 66 | error: `${err.message}` 67 | }) 68 | } 69 | } 70 | 71 | if (typeof result.data === 'undefined') { 72 | // createFile(octokit, config, file, content) 73 | // throw file doesnt exist 74 | return { 75 | statusCode: 400, 76 | body: JSON.stringify({ 77 | error: `No ${FILE_TO_CHANGE} found` 78 | }) 79 | } 80 | } 81 | 82 | // content will be base64 encoded 83 | const content = Buffer.from(result.data.content, 'base64').toString() 84 | 85 | const allData = parseFile(FILE_TO_CHANGE, content) 86 | console.log('allData.length', allData.length) 87 | 88 | if (alreadyHasUri(body, allData)) { 89 | console.log(`${body.url} already is in the list!`) 90 | return { 91 | statusCode: 422, 92 | body: JSON.stringify({ 93 | message: `${body.url} already is in the list!` 94 | }) 95 | } 96 | } 97 | 98 | const newData = allData.concat(body) 99 | const newContent = JSON.stringify(newData, null, 2) 100 | 101 | let response = {} 102 | try { 103 | response = await octokit.createPullRequest({ 104 | owner, 105 | repo, 106 | title: `Add tutorial ${body.url}`, 107 | body: `Add ${body.title} at ${body.url}`, 108 | base: 'master', /* optional: defaults to default branch */ 109 | head: `pull-request-branch-name-${new Date().getTime()}`, 110 | changes: { 111 | files: { 112 | [`${FILE_TO_CHANGE}`]: newContent, 113 | }, 114 | commit: `updating ${FILE_TO_CHANGE}` 115 | } 116 | }) 117 | } catch (err) { 118 | if (err.status === 422) { 119 | console.log('BRANCH ALREADY EXISTS!') 120 | return { 121 | statusCode: 400, 122 | body: JSON.stringify({ 123 | error: `BRANCH ALREADY EXISTS!` 124 | }) 125 | } 126 | } 127 | } 128 | console.log('data', response.data) 129 | return { 130 | statusCode: 200, 131 | body: JSON.stringify({ 132 | message: `pr created!`, 133 | url: response.data.html_url 134 | }) 135 | } 136 | } 137 | 138 | /** 139 | * Check if array already has URL 140 | * @return {Boolean} 141 | */ 142 | function alreadyHasUri(newItem, allData) { 143 | return allData.some((item) => { 144 | if (!item.url || !newItem.url) { 145 | return false 146 | } 147 | return niceUrl(item.url) === niceUrl(newItem.url) 148 | }) 149 | } 150 | 151 | function niceUrl(href) { 152 | const { host, pathname } = url.parse(href) 153 | return `${host}${pathname}` 154 | } 155 | 156 | /** 157 | * Stringify to JSON maybe. 158 | * 159 | * @param {string} file 160 | * @param {string} content 161 | * 162 | * @return {string} 163 | */ 164 | function parseFile(file, content) { 165 | if (file.indexOf('.json') !== -1) { 166 | return JSON.parse(content) 167 | } 168 | 169 | return content 170 | } 171 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "add-example.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@octokit/rest": "^16.3.0", 13 | "git-url-parse": "^11.1.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /functions/utils/createPullRequest.js: -------------------------------------------------------------------------------- 1 | module.exports = octokitCreatePullRequest 2 | 3 | function octokitCreatePullRequest (octokit) { 4 | octokit.createPullRequest = createPullRequest.bind(null, octokit) 5 | } 6 | 7 | async function createPullRequest (octokit, { owner, repo, title, body, base, head, changes }) { 8 | let response 9 | 10 | if (!base) { 11 | response = await octokit.repos.get({ owner, repo }) 12 | base = response.data.default_branch 13 | } 14 | 15 | response = await octokit.repos.listCommits({ 16 | owner, 17 | repo, 18 | sha: base, 19 | per_page: 1 20 | }) 21 | let latestCommitSha = response.data[0].sha 22 | const treeSha = response.data[0].commit.tree.sha 23 | 24 | response = await octokit.git.createTree({ 25 | owner, 26 | repo, 27 | base_tree: treeSha, 28 | tree: Object.keys(changes.files).map(path => { 29 | return { 30 | path, 31 | mode: '100644', 32 | content: changes.files[path] 33 | } 34 | }) 35 | }) 36 | const newTreeSha = response.data.sha 37 | 38 | response = await octokit.git.createCommit({ 39 | owner, 40 | repo, 41 | message: changes.commit, 42 | tree: newTreeSha, 43 | parents: [latestCommitSha] 44 | }) 45 | latestCommitSha = response.data.sha 46 | 47 | await octokit.git.createRef({ 48 | owner, 49 | repo, 50 | sha: latestCommitSha, 51 | ref: `refs/heads/${head}` 52 | }) 53 | 54 | response = await octokit.pulls.create({ 55 | owner, 56 | repo, 57 | head, 58 | base, 59 | title, 60 | body 61 | }) 62 | return response 63 | } 64 | -------------------------------------------------------------------------------- /functions/utils/sanitize.js: -------------------------------------------------------------------------------- 1 | 2 | // Add -1); waitfor delay '0:0:9.456' -- at 3 | // Add -1;select pg_sleep(14.184); -- at 4 | // Add 1'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),4.728)||' at 5 | // Add '.print(md5(31337)).' at 6 | module.exports = function isClean(str) { 7 | const value = str || '' 8 | if (value.length < 3) { 9 | return false 10 | } 11 | const matches = value.match(/\.\.\/|bxss\.me|\|\|\);|';|0:0:|\)\)|\(\(|\(\),|\.xml|\/\/\/|\.\.%/g) 12 | if (matches) { 13 | return false 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | const analytics = require('./src/analytics').default 2 | 3 | exports.onRouteUpdate = ({ location }) => { 4 | console.log('new pathname', location.pathname) 5 | console.log('analytics', analytics) 6 | } -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const urljoin = require('url-join') 2 | const config = require('./_site-config') 3 | const isDevelopment = process.env.NODE_ENV !== `production` 4 | 5 | /* hot module reloading for CSS variables */ 6 | const postcssFile = require.resolve("./postcss.config.js") 7 | const postcssPlugins = (webpackInstance) => { 8 | const varFile = require.resolve("./src/_variables.js") 9 | const varFileContents = () => { 10 | webpackInstance.addDependency(varFile) 11 | delete require.cache[varFile] 12 | return require(varFile) 13 | } 14 | webpackInstance.addDependency(postcssFile) 15 | delete require.cache[postcssFile] 16 | return require(postcssFile)({}, varFileContents()) 17 | } 18 | 19 | module.exports = { 20 | pathPrefix: config.pathPrefix, 21 | siteMetadata: { 22 | siteUrl: urljoin(config.siteUrl, config.pathPrefix), 23 | rssMetadata: { 24 | site_url: urljoin(config.siteUrl, config.pathPrefix), 25 | feed_url: urljoin(config.siteUrl, config.pathPrefix, config.siteRss), 26 | title: config.siteTitle, 27 | description: config.siteDescription, 28 | image_url: `${urljoin(config.siteUrl, config.pathPrefix)}/logos/logo-512.png`, 29 | author: config.userName, 30 | copyright: config.copyright 31 | } 32 | }, 33 | plugins: [ 34 | { 35 | resolve: 'gatsby-route-plugin', 36 | options: { 37 | debug: true 38 | } 39 | }, 40 | { 41 | resolve: 'gatsby-plugin-analytics', 42 | options: { 43 | debug: true 44 | } 45 | }, 46 | 'gatsby-plugin-react-helmet', 47 | 'gatsby-plugin-lodash', 48 | // https://github.com/gatsbyjs/gatsby/pull/8496/files 49 | // { 50 | // resolve: `gatsby-plugin-react-css-modules`, 51 | // options: { 52 | // generateScopedName: isDevelopment ? `[name]--[local]--[hash:base64:5]` : `[hash:base64:5]`, 53 | // }, 54 | // }, 55 | { 56 | resolve: 'gatsby-better-postcss', 57 | options: { 58 | cssMatch: 'hi', 59 | postCssPlugins: postcssPlugins, 60 | }, 61 | }, 62 | { 63 | resolve: 'gatsby-source-filesystem', 64 | options: { 65 | name: 'assets', 66 | path: `${__dirname}/static/` 67 | } 68 | }, 69 | { 70 | resolve: 'gatsby-source-filesystem', 71 | options: { 72 | name: 'posts', 73 | path: `${__dirname}/content/` 74 | } 75 | }, 76 | { 77 | resolve: 'gatsby-transformer-remark', 78 | options: { 79 | plugins: [ 80 | { 81 | resolve: 'gatsby-remark-images', 82 | options: { 83 | maxWidth: 690 84 | } 85 | }, 86 | { 87 | resolve: 'gatsby-remark-responsive-iframe' 88 | }, 89 | 'gatsby-remark-prismjs', 90 | 'gatsby-remark-copy-linked-files', 91 | 'gatsby-remark-autolink-headers' 92 | ] 93 | } 94 | }, 95 | { 96 | resolve: 'gatsby-plugin-google-analytics', 97 | options: { 98 | trackingId: config.googleAnalyticsID 99 | } 100 | }, 101 | { 102 | resolve: 'gatsby-plugin-nprogress', 103 | options: { 104 | color: config.themeColor 105 | } 106 | }, 107 | 'gatsby-plugin-sharp', 108 | 'gatsby-plugin-catch-links', 109 | 'gatsby-plugin-twitter', 110 | 'gatsby-plugin-sitemap', 111 | { 112 | resolve: 'gatsby-plugin-manifest', 113 | options: { 114 | name: config.siteTitle, 115 | short_name: config.siteTitleShort, 116 | description: config.siteDescription, 117 | start_url: config.pathPrefix, 118 | background_color: config.backgroundColor, 119 | theme_color: config.themeColor, 120 | display: 'minimal-ui', 121 | icons: [ 122 | { 123 | src: '/logos/logo-192x192.png', 124 | sizes: '192x192', 125 | type: 'image/png' 126 | }, 127 | { 128 | src: '/logos/logo-512x512.png', 129 | sizes: '512x512', 130 | type: 'image/png' 131 | } 132 | ] 133 | } 134 | }, 135 | // 'gatsby-plugin-offline', 136 | { 137 | resolve: 'gatsby-plugin-feed', 138 | options: { 139 | setup(ref) { 140 | const ret = ref.query.site.siteMetadata.rssMetadata 141 | ret.allMarkdownRemark = ref.query.allMarkdownRemark 142 | ret.generator = 'GatsbyJS Material Starter' 143 | return ret 144 | }, 145 | query: ` 146 | { 147 | site { 148 | siteMetadata { 149 | rssMetadata { 150 | site_url 151 | feed_url 152 | title 153 | description 154 | image_url 155 | author 156 | copyright 157 | } 158 | } 159 | } 160 | } 161 | `, 162 | feeds: [ 163 | { 164 | serialize(ctx) { 165 | const { rssMetadata } = ctx.query.site.siteMetadata 166 | return ctx.query.allMarkdownRemark.edges.map(edge => ({ 167 | categories: edge.node.frontmatter.tags, 168 | date: edge.node.fields.date, 169 | title: edge.node.frontmatter.title, 170 | description: edge.node.excerpt, 171 | author: rssMetadata.author, 172 | url: rssMetadata.site_url + edge.node.fields.slug, 173 | guid: rssMetadata.site_url + edge.node.fields.slug, 174 | custom_elements: [{ 'content:encoded': edge.node.html }] 175 | })) 176 | }, 177 | query: ` 178 | { 179 | allMarkdownRemark( 180 | limit: 1000, 181 | sort: { order: DESC, fields: [fields___date] }, 182 | ) { 183 | edges { 184 | node { 185 | excerpt 186 | html 187 | timeToRead 188 | fields { 189 | slug 190 | date 191 | } 192 | frontmatter { 193 | title 194 | cover 195 | date 196 | category 197 | tags 198 | } 199 | } 200 | } 201 | } 202 | } 203 | `, 204 | output: config.siteRss 205 | } 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const _ = require('lodash') 3 | const moment = require('moment') 4 | const siteConfig = require('./_site-config') 5 | const { createFilePath } = require(`gatsby-source-filesystem`) 6 | 7 | const postNodes = [] 8 | 9 | // Fetching remote data https://github.com/gatsbyjs/gatsby/blob/5a08b7640c2db2c65696be56d81afee83b2ca9ec/examples/using-unstructured-data/gatsby-node.js 10 | // Big example https://github.com/gatsbyjs/gatsby/blob/master/www/gatsby-node.js 11 | 12 | function addSiblingNodes(createNodeField) { 13 | postNodes.sort( 14 | ({ frontmatter: { date: date1 } }, { frontmatter: { date: date2 } }) => { 15 | const dateA = moment(date1, siteConfig.dateFromFormat) 16 | const dateB = moment(date2, siteConfig.dateFromFormat) 17 | 18 | if (dateA.isBefore(dateB)) return 1 19 | 20 | if (dateB.isBefore(dateA)) return -1 21 | 22 | return 0 23 | } 24 | ) 25 | for (let i = 0; i < postNodes.length; i += 1) { 26 | const nextID = i + 1 < postNodes.length ? i + 1 : 0 27 | const prevID = i - 1 > 0 ? i - 1 : postNodes.length - 1 28 | const currNode = postNodes[i] 29 | const nextNode = postNodes[nextID] 30 | const prevNode = postNodes[prevID] 31 | createNodeField({ 32 | node: currNode, 33 | name: 'nextTitle', 34 | value: nextNode.frontmatter.title 35 | }) 36 | createNodeField({ 37 | node: currNode, 38 | name: 'nextSlug', 39 | value: nextNode.fields.slug 40 | }) 41 | createNodeField({ 42 | node: currNode, 43 | name: 'prevTitle', 44 | value: prevNode.frontmatter.title 45 | }) 46 | createNodeField({ 47 | node: currNode, 48 | name: 'prevSlug', 49 | value: prevNode.fields.slug 50 | }) 51 | } 52 | } 53 | 54 | exports.onCreateNode = ({ node, actions, getNode }) => { 55 | const { createNodeField } = actions 56 | let slug 57 | 58 | if (node.internal.type === `MarkdownRemark`) { 59 | const fileNode = getNode(node.parent) 60 | console.log(`\n`, fileNode.relativePath) 61 | const test = createFilePath({ node, getNode, basePath: `pages` }) 62 | console.log('test', test) 63 | } 64 | if (node.internal.type === 'MarkdownRemark') { 65 | const fileNode = getNode(node.parent) 66 | const parsedFilePath = path.parse(fileNode.relativePath) 67 | if ( 68 | Object.prototype.hasOwnProperty.call(node, 'frontmatter') && 69 | Object.prototype.hasOwnProperty.call(node.frontmatter, 'title') 70 | ) { 71 | console.log('Set title as slug', node.frontmatter.title) 72 | slug = `/${_.kebabCase(node.frontmatter.title)}` 73 | } else if (parsedFilePath.name !== 'index' && parsedFilePath.dir !== '') { 74 | slug = `/${parsedFilePath.dir}/${parsedFilePath.name}/` 75 | } else if (parsedFilePath.dir === '') { 76 | slug = `/${parsedFilePath.name}/` 77 | } else { 78 | slug = `/${parsedFilePath.dir}/` 79 | } 80 | 81 | if (Object.prototype.hasOwnProperty.call(node, 'frontmatter')) { 82 | if (Object.prototype.hasOwnProperty.call(node.frontmatter, 'slug')) { 83 | slug = `/${_.kebabCase(node.frontmatter.slug)}` 84 | } 85 | 86 | if (Object.prototype.hasOwnProperty.call(node.frontmatter, 'date')) { 87 | const date = moment(node.frontmatter.date, siteConfig.dateFromFormat) 88 | if (!date.isValid) { console.warn(`WARNING: Invalid date.`, node.frontmatter) } 89 | 90 | createNodeField({ 91 | node, 92 | name: 'date', 93 | value: date.toISOString() 94 | }) 95 | } 96 | } 97 | createNodeField({ node, name: 'slug', value: slug }) 98 | postNodes.push(node) 99 | } 100 | } 101 | 102 | exports.setFieldsOnGraphQLNodeType = ({ type, actions }) => { 103 | const { name } = type 104 | const { createNodeField } = actions 105 | if (name === 'MarkdownRemark') { 106 | addSiblingNodes(createNodeField) 107 | } 108 | } 109 | 110 | exports.createPages = ({ graphql, actions }) => { 111 | const { createPage } = actions 112 | 113 | return new Promise((resolve, reject) => { 114 | const postPage = path.resolve('src/templates/Post.js') 115 | const tagPage = path.resolve('src/templates/Tag.js') 116 | const categoryPage = path.resolve('src/templates/Category.js') 117 | resolve( 118 | graphql( 119 | ` 120 | { 121 | allMarkdownRemark { 122 | edges { 123 | node { 124 | frontmatter { 125 | tags 126 | category 127 | } 128 | fields { 129 | slug 130 | } 131 | } 132 | } 133 | } 134 | } 135 | ` 136 | ).then(result => { 137 | if (result.errors) { 138 | /* eslint no-console: "off" */ 139 | console.log(result.errors) 140 | reject(result.errors) 141 | } 142 | 143 | const tagSet = new Set() 144 | const categorySet = new Set() 145 | result.data.allMarkdownRemark.edges.forEach(edge => { 146 | if (edge.node.frontmatter.tags) { 147 | edge.node.frontmatter.tags.forEach(tag => { 148 | tagSet.add(tag) 149 | }) 150 | } 151 | 152 | if (edge.node.frontmatter.category) { 153 | categorySet.add(edge.node.frontmatter.category) 154 | } 155 | 156 | createPage({ 157 | path: edge.node.fields.slug, 158 | component: postPage, 159 | context: { 160 | slug: edge.node.fields.slug 161 | } 162 | }) 163 | }) 164 | 165 | const tagList = Array.from(tagSet) 166 | tagList.forEach(tag => { 167 | createPage({ 168 | path: `/tags/${_.kebabCase(tag)}/`, 169 | component: tagPage, 170 | context: { 171 | tag 172 | } 173 | }) 174 | }) 175 | 176 | const categoryList = Array.from(categorySet) 177 | categoryList.forEach(category => { 178 | createPage({ 179 | path: `/categories/${_.kebabCase(category)}/`, 180 | component: categoryPage, 181 | context: { 182 | category 183 | } 184 | }) 185 | }) 186 | }) 187 | ) 188 | }) 189 | } 190 | 191 | /* Shrink CSS class names in prod */ 192 | const cssLoaderRe = /\/css-loader\//; 193 | const targetFile = '.css'; 194 | 195 | const processRule = rule => { 196 | // console.log('rule', rule) 197 | if (rule.oneOf) { 198 | return { 199 | ...rule, 200 | oneOf: rule.oneOf.map(processRule), 201 | }; 202 | } 203 | 204 | if (!rule.test.test(targetFile)) { 205 | return rule; 206 | } 207 | 208 | if (Array.isArray(rule.use)) { 209 | return { 210 | ...rule, 211 | use: rule.use.map(use => { 212 | if (!cssLoaderRe.test(use.loader)) { 213 | return use; 214 | } 215 | // console.log('use', use) 216 | // Adjust css-loader options 217 | return { 218 | ...use, 219 | options: { 220 | ...use.options, 221 | localIdentName: 222 | process.env.NODE_ENV === 'production' 223 | ? '[hash:base64:5]' 224 | : '[name]_[local]_[hash:base64:4]', 225 | }, 226 | }; 227 | }), 228 | }; 229 | } 230 | 231 | return rule; 232 | }; 233 | 234 | exports.onCreateWebpackConfig = ({ getConfig, actions }) => { 235 | const config = getConfig(); 236 | 237 | const newConfig = { 238 | ...config, 239 | module: { 240 | ...config.module, 241 | rules: config.module.rules.map(processRule), 242 | }, 243 | }; 244 | actions.replaceWebpackConfig(newConfig); 245 | }; 246 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | command = "npm run build" 4 | functions = "functions" 5 | 6 | # Redirects 7 | [[redirects]] 8 | from = "/add" 9 | to = "/add-example" 10 | status = 301 11 | force = true 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-functions", 3 | "description": "Netlify functions site", 4 | "version": "0.0.1", 5 | "private": true, 6 | "keywords": [ 7 | "gatsby" 8 | ], 9 | "author": "David Wells ", 10 | "license": "MIT", 11 | "main": "n/a", 12 | "scripts": { 13 | "develop": "gatsby develop", 14 | "dev": "npm run develop", 15 | "start": "npm run dev", 16 | "serve": "gatsby serve", 17 | "build": "gatsby build", 18 | "clean": "rm -rf public && rm -rf .cache", 19 | "lint:js": "eslint --ext .js,.jsx .", 20 | "lint:md": "remark content/posts/", 21 | "write-good": "write-good $(glob 'content/posts/**/*.md')" 22 | }, 23 | "dependencies": { 24 | "analytics": "^0.1.2", 25 | "analytics-plugin-google-tag-manager": "^0.1.0", 26 | "analytics-plugin-segment": "^0.1.1", 27 | "analytics-utils": "0.0.13", 28 | "classnames": "^2.2.6", 29 | "clipboard": "^1.7.1", 30 | "cssnano": "^4.1.10", 31 | "gatsby": "2.2.11", 32 | "gatsby-image": "2.0.34", 33 | "gatsby-plugin-analytics": "0.0.3", 34 | "gatsby-plugin-catch-links": "2.0.13", 35 | "gatsby-plugin-feed": "2.1.0", 36 | "gatsby-plugin-google-analytics": "2.0.17", 37 | "gatsby-plugin-lodash": "3.0.5", 38 | "gatsby-plugin-manifest": "2.0.24", 39 | "gatsby-plugin-nprogress": "2.0.10", 40 | "gatsby-plugin-offline": "2.0.25", 41 | "gatsby-plugin-react-helmet": "3.0.11", 42 | "gatsby-plugin-sharp": "2.0.31", 43 | "gatsby-plugin-sitemap": "2.0.10", 44 | "gatsby-plugin-twitter": "2.0.13", 45 | "gatsby-remark-autolink-headers": "2.0.16", 46 | "gatsby-remark-copy-linked-files": "2.0.11", 47 | "gatsby-remark-images": "3.0.10", 48 | "gatsby-remark-prismjs": "3.2.6", 49 | "gatsby-remark-responsive-iframe": "2.1.1", 50 | "gatsby-source-filesystem": "2.0.28", 51 | "gatsby-transformer-remark": "2.1.15", 52 | "gatsby-transformer-sharp": "2.1.17", 53 | "get-form-data": "^2.0.0", 54 | "icon-pipeline": "0.0.3", 55 | "lodash": "^4.17.11", 56 | "lodash.debounce": "^4.0.8", 57 | "moment": "^2.24.0", 58 | "netlify-identity-widget": "^1.5.2", 59 | "postcss-cssnext": "^3.1.0", 60 | "postcss-nested": "^4.1.2", 61 | "postcss-simple-vars": "^5.0.2", 62 | "prismjs": "^1.16.0", 63 | "prop-types": "^15.7.2", 64 | "react": "^16.8.5", 65 | "react-disqus-comments": "^1.4.0", 66 | "react-dom": "^16.8.5", 67 | "react-helmet": "^5.2.0", 68 | "react-select": "^2.4.2", 69 | "url-join": "^4.0.0" 70 | }, 71 | "devDependencies": { 72 | "cli-glob": "^0.1.0", 73 | "eslint": "^5.8.0", 74 | "eslint-config-airbnb": "^17.1.0", 75 | "eslint-config-prettier": "^4.1.0", 76 | "eslint-plugin-import": "^2.16.0", 77 | "eslint-plugin-jsx-a11y": "^6.2.1", 78 | "eslint-plugin-react": "^7.12.4", 79 | "prettier": "^1.16.4", 80 | "remark-cli": "^6.0.1", 81 | "remark-preset-lint-recommended": "^3.0.2", 82 | "stylefmt": "^6.0.3", 83 | "stylelint": "^9.10.1", 84 | "stylelint-config-standard": "^18.2.0", 85 | "write-good": "^1.0.1" 86 | }, 87 | "remarkConfig": { 88 | "plugins": [ 89 | "remark-preset-lint-recommended" 90 | ] 91 | }, 92 | "eslintIgnore": [ 93 | "public", 94 | "static", 95 | ".cache", 96 | "content" 97 | ], 98 | "esLintConfig": { 99 | "extends": [ 100 | "airbnb", 101 | "prettier" 102 | ], 103 | "plugins": [ 104 | "react", 105 | "jsx-a11y", 106 | "import" 107 | ], 108 | "rules": { 109 | "quotes": [ 110 | 2, 111 | "single", 112 | { 113 | "avoidEscape": true 114 | } 115 | ], 116 | "react/prefer-stateless-function": "off", 117 | "react/prop-types": "off", 118 | "react/no-danger": "off", 119 | "jsx-a11y/anchor-is-valid": [ 120 | "error", 121 | { 122 | "components": [ 123 | "Link" 124 | ], 125 | "specialLink": [ 126 | "hrefLeft", 127 | "hrefRight", 128 | "to" 129 | ], 130 | "aspects": [ 131 | "noHref", 132 | "invalidHref", 133 | "preferButton" 134 | ] 135 | } 136 | ] 137 | }, 138 | "settings": { 139 | "import/core-modules": [] 140 | }, 141 | "env": { 142 | "browser": true 143 | } 144 | }, 145 | "stylelint": { 146 | "extends": "stylelint-config-standard", 147 | "rules": { 148 | "indentation": 4 149 | } 150 | }, 151 | "browserslist": [ 152 | "Last 2 versions" 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /plugins/gatsby-better-postcss/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const resolve = module => require.resolve(module) 2 | 3 | const CSS_PATTERN = /\.css$/ 4 | const MODULE_CSS_PATTERN = /\.module\.css$/ 5 | const GLOBAL_CSS_PATTERN = /\.global\.css$/ 6 | 7 | const getOptions = pluginOptions => { 8 | const options = { ...pluginOptions } 9 | 10 | delete options.plugins 11 | 12 | const postcssPlugins = options.postCssPlugins 13 | 14 | if (postcssPlugins) { 15 | options.plugins = postcssPlugins 16 | } 17 | 18 | delete options.postCssPlugins 19 | 20 | return options 21 | } 22 | 23 | const isCssRules = rule => 24 | rule.test && 25 | (rule.test.toString() === CSS_PATTERN.toString() || 26 | rule.test.toString() === MODULE_CSS_PATTERN.toString()) 27 | 28 | const findCssRules = config => 29 | config.module.rules.find( 30 | rule => Array.isArray(rule.oneOf) && rule.oneOf.every(isCssRules) 31 | ) 32 | 33 | exports.onCreateWebpackConfig = ( 34 | { actions, stage, loaders, getConfig }, 35 | pluginOptions 36 | ) => { 37 | const isProduction = !stage.includes(`develop`) 38 | // console.log('isProduction', isProduction) 39 | // console.log('pluginOptions', pluginOptions) 40 | const isSSR = stage.includes(`html`) 41 | const config = getConfig() 42 | const cssRules = findCssRules(config) 43 | const postcssOptions = getOptions(pluginOptions) 44 | const postcssLoader = { 45 | loader: resolve(`postcss-loader`), 46 | options: { sourceMap: !isProduction, ...postcssOptions }, 47 | } 48 | const postcssRule = { 49 | test: GLOBAL_CSS_PATTERN, 50 | use: isSSR 51 | ? [loaders.null()] 52 | : [loaders.css({ importLoaders: 1 }), postcssLoader], 53 | } 54 | const postcssRuleModules = { 55 | test: CSS_PATTERN, 56 | use: [ 57 | loaders.css({ 58 | modules: true, 59 | importLoaders: 1, 60 | }), 61 | postcssLoader, 62 | ], 63 | exclude: GLOBAL_CSS_PATTERN 64 | } 65 | 66 | if (!isSSR) { 67 | postcssRule.use.unshift(loaders.miniCssExtract()) 68 | postcssRuleModules.use.unshift(loaders.miniCssExtract()) 69 | } 70 | 71 | const postcssRules = { oneOf: [] } 72 | 73 | switch (stage) { 74 | case `develop`: 75 | case `build-javascript`: 76 | case `build-html`: 77 | case `develop-html`: 78 | postcssRules.oneOf.push(...[postcssRuleModules, postcssRule]) 79 | break 80 | } 81 | 82 | if (cssRules) { 83 | cssRules.oneOf.unshift(...postcssRules.oneOf) 84 | 85 | actions.replaceWebpackConfig(config) 86 | } else { 87 | actions.setWebpackConfig({ module: { rules: [postcssRules] } }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /plugins/gatsby-better-postcss/index.js: -------------------------------------------------------------------------------- 1 | // noop -------------------------------------------------------------------------------- /plugins/gatsby-better-postcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-better-postcss" 3 | } -------------------------------------------------------------------------------- /plugins/gatsby-route-plugin/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /* global Analytics */ 2 | 3 | exports.onRouteUpdate = function({ location }, options) { 4 | console.log('location', location) 5 | } 6 | -------------------------------------------------------------------------------- /plugins/gatsby-route-plugin/index.js: -------------------------------------------------------------------------------- 1 | // noop -------------------------------------------------------------------------------- /plugins/gatsby-route-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-route-plugin" 3 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // Hot reloading variables 2 | module.exports = (config, hotLoadedVariables) => [ 3 | require('postcss-cssnext')({ browsers: 'last 2 versions' }), 4 | require('postcss-simple-vars')({ 5 | variables: function variables() { 6 | return hotLoadedVariables 7 | }, 8 | onVariables(variables) { 9 | // console.log(variables) 10 | }, 11 | unknown: function unknown(node, name, result) { 12 | node.warn(result, `Unknown variable ${name}`) 13 | } 14 | }), 15 | require('postcss-nested'), 16 | require('cssnano')() 17 | ] -------------------------------------------------------------------------------- /src/_variables.js: -------------------------------------------------------------------------------- 1 | /**** 2 | Global CSS variables for use in CSS and JS 3 | 4 | ## CSS usage: 5 | 6 | ```css 7 | backgound: $varName 8 | ``` 9 | 10 | ## JS usage: 11 | 12 | ```js 13 | import variables from '../' 14 | 15 | const { blue } = variables 16 | ``` 17 | ****/ 18 | 19 | const baseValue = 1 20 | const unit = 'rem' 21 | const baseFontSize = (baseValue * 1.6) + unit 22 | 23 | module.exports = { 24 | textSelection: '#80cbbf', 25 | // -- Colors 26 | primary: '#00ad9f', 27 | primaryHover: '#00c2b2', 28 | secondary: '#f5f8f9', 29 | grey: '#8b8b8b', 30 | danger: '#fb6d77', 31 | dangerHover: '#fa3d4a', 32 | // -- Icon sizes 33 | iconDefault: '35px', 34 | // -- Fonts 35 | fontSize: baseFontSize, 36 | fontSizeTiny: formatFont(1.2), 37 | fontSizeSmall: formatFont(1.4), 38 | fontSizeNormal: baseFontSize, 39 | fontSizeBig: formatFont(1.8), 40 | fontSizeH1: formatFont(3.0), 41 | fontSizeH2: formatFont(2.15), 42 | fontSizeH3: formatFont(1.7), 43 | fontSizeH4: formatFont(1.25), 44 | fontSizeH5: baseFontSize, 45 | fontSizeH6: formatFont(0.85), 46 | // -- Indexes 47 | zIndexHighest: 300, 48 | zIndexHigher: 200, 49 | zIndexHigh: 100, 50 | zIndexNormal: 1, 51 | zIndexLow: -100, 52 | zIndexLower: -200, 53 | } 54 | 55 | function formatFont(modifier) { 56 | return (modifier * baseValue) + unit 57 | } 58 | -------------------------------------------------------------------------------- /src/analytics.js: -------------------------------------------------------------------------------- 1 | import Analytics from 'analytics' 2 | import segmentPlugin from 'analytics-plugin-segment' 3 | import gtagManagerPlugin from 'analytics-plugin-google-tag-manager' 4 | 5 | const analytics = Analytics({ 6 | plugins: [ 7 | gtagManagerPlugin({ 8 | containerId: 'GTM-NMKKF2M' 9 | }), 10 | segmentPlugin({ 11 | writeKey: 'f3W8BZ0iCGrk1STIsMZV7JXfMGB7aMiW', 12 | disableAnonymousTraffic: true, 13 | }), 14 | { 15 | NAMESPACE: 'custom-analytics-plugin', 16 | page: ({ payload }) => { 17 | const { protocol, host, pathname } = window.location 18 | const { properties, meta, anonymousId, userId } = payload 19 | setTimeout(() => { 20 | const analyticsPayload = Object.assign({}, properties, { 21 | date: meta.timestamp || new Date().getTime(), 22 | title: properties.title || document.title, 23 | url: `${protocol}//${host}${pathname}`, 24 | anonymousId: anonymousId, 25 | userId: userId, 26 | }) 27 | console.log('payload', analyticsPayload) 28 | if (window.location.origin === 'https://functions.netlify.com') { 29 | const endpoint = 'https://07z2fk5eb4.execute-api.us-west-2.amazonaws.com/prod/collect' 30 | fetch(endpoint, { 31 | method: 'POST', 32 | headers: new Headers({ 33 | 'Content-Type': 'application/json' 34 | }), 35 | body: JSON.stringify(analyticsPayload) 36 | }) 37 | } 38 | }, 0) 39 | } 40 | } 41 | ] 42 | }) 43 | 44 | analytics.on('page', ({ payload }) => { 45 | console.log('page', payload) 46 | }) 47 | 48 | analytics.on('track', ({ payload }) => { 49 | console.log('track', payload) 50 | }) 51 | 52 | // Set to global so analytics plugin will work 53 | if (typeof window !== 'undefined') { 54 | window.Analytics = analytics 55 | } 56 | 57 | export default analytics 58 | -------------------------------------------------------------------------------- /src/components/Button/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | background-color: #424242; 4 | font-family: inherit; 5 | border: none; 6 | color: #fff; 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 13px 18px; 11 | font-size: 15px; 12 | border: 1px solid #e9ebeb; 13 | border-bottom: 1px solid #e1e3e3; 14 | border-radius: 4px; 15 | background-color: #fff; 16 | color: rgba(14,30,37,.87); 17 | box-shadow: 0 2px 4px 0 rgba(14,30,37,.12); 18 | transition: all .2s ease; 19 | transition-property: background-color,color,border,box-shadow; 20 | outline: 0; 21 | font-weight: 500; 22 | &.hasIcon { 23 | .buttonText { 24 | padding-top: 3px; 25 | } 26 | } 27 | &:hover, &:focus { 28 | box-shadow: 0 8px 12px 0 rgba(233,235,235,.16), 0 2px 8px 0 rgba(0,0,0,.08); 29 | text-decoration: none; 30 | } 31 | /* kind styles */ 32 | &.primary { 33 | background: $primary; 34 | color: #fff; 35 | border-color: transparent; 36 | &:hover, &:focus { 37 | background: $primaryHover; 38 | } 39 | } 40 | &.secondary { 41 | background: #424242; 42 | color: #fff; 43 | } 44 | &.tertiary { 45 | background: #fff; 46 | color: rgba(14,30,37,.87); 47 | &:hover, &:focus { 48 | background-color: #f5f5f5; 49 | color: rgba(14,30,37,.87); 50 | } 51 | } 52 | &.danger { 53 | background: $danger; 54 | color: #fff; 55 | &:hover, &:focus { 56 | background: $dangerHover; 57 | } 58 | } 59 | &:focus { 60 | outline: none; 61 | } 62 | } 63 | 64 | a.button { 65 | display: inline-flex; 66 | text-decoration: none; 67 | color: #fff; 68 | border: none !important; 69 | } 70 | .icon { 71 | margin-right: 10px; 72 | fill: #fff; 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import { Link } from 'gatsby' 5 | import Icon from '../Icon' 6 | import styles from './Button.css' 7 | 8 | const propTypes = { 9 | /** Custom CSS Classes */ 10 | className: PropTypes.string, 11 | /** onClick handler */ 12 | onClick: PropTypes.func, 13 | /** Text on the button */ 14 | label: PropTypes.string, 15 | /** Element inside the button */ 16 | children: PropTypes.any, // eslint-disable-line 17 | /** Determines the style of the button * */ 18 | kind: PropTypes.oneOf(['primary', 'secondary', 'tertiary', 'danger']), 19 | /** Custom style prop */ 20 | style: PropTypes.object, 21 | /** if href provided to button, button will be a link */ 22 | href: PropTypes.string, 23 | /** target of href */ 24 | target: PropTypes.string, 25 | /** name of icon */ 26 | icon: PropTypes.string, 27 | /** Size of Icon. Takes number or pixel value. Example size={30} */ 28 | iconSize: PropTypes.oneOfType([ 29 | PropTypes.string, 30 | PropTypes.number 31 | ]), 32 | } 33 | 34 | const defaultProps = { 35 | kind: 'primary', 36 | iconSize: 24 37 | } 38 | 39 | /** 40 | * Protip: Use `kind` prop to set styles of button 41 | */ 42 | export default function Button({ 43 | onClick, 44 | label, 45 | children, 46 | className, 47 | kind, 48 | style, 49 | href, 50 | to, 51 | target, 52 | icon, 53 | iconSize, 54 | ...props 55 | }) { 56 | const text = label || children 57 | 58 | const classes = classNames( 59 | 'component-button', 60 | className, 61 | styles.button, 62 | styles[kind], 63 | { 64 | [styles.hasIcon]: icon 65 | } 66 | ) 67 | 68 | let iconRender 69 | if (icon) { 70 | iconRender = ( 71 | 72 | ) 73 | } 74 | // Make link if href supplied 75 | if (href) { 76 | return ( 77 | 84 | {iconRender} 85 | {text} 86 | 87 | ) 88 | } 89 | if (to) { 90 | return ( 91 | 98 | {iconRender} 99 | {text} 100 | 101 | ) 102 | } 103 | 104 | return ( 105 | 111 | ) 112 | } 113 | 114 | Button.propTypes = propTypes 115 | Button.defaultProps = defaultProps 116 | -------------------------------------------------------------------------------- /src/components/Card/Card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 10px; 3 | display: flex; 4 | cursor: pointer; 5 | } 6 | .cardInner { 7 | padding: 20px; 8 | border-radius: 2px; 9 | background: white; 10 | box-shadow: 0 2px 1px rgba(170, 170, 170, 0.25); 11 | display: flex; 12 | align-items: flex-start; 13 | flex-direction: column; 14 | width: 100%; 15 | transition: .1s ease-out; 16 | position: relative; 17 | &:hover { 18 | box-shadow: 0 2px 1px rgba(170, 170, 170, 0.45); 19 | transform: scale(1.05); 20 | transition: .1s ease-out; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import styles from './Card.css' 4 | 5 | const Card = (props) => { 6 | const classes = classnames(props.className, styles.card) 7 | return ( 8 |
9 |
10 | {props.children} 11 |
12 |
13 | ) 14 | } 15 | 16 | export default Card 17 | -------------------------------------------------------------------------------- /src/components/Copy/Copy.css: -------------------------------------------------------------------------------- 1 | .copy { 2 | &:after { 3 | opacity: .4; 4 | visibility: hidden; 5 | position: absolute; 6 | content: "copy link"; 7 | text-align: center; 8 | height: 20px; 9 | transform: none; 10 | width: 55px; 11 | font-size: 11px; 12 | padding: 3px 5px; 13 | display: -ms-flexbox; 14 | display: flex; 15 | -ms-flex-align: center; 16 | align-items: center; 17 | -ms-flex-pack: center; 18 | justify-content: center; 19 | color: white; 20 | background-color: #191919; 21 | top: 85%; 22 | left: 8px; 23 | border-radius: 3px; 24 | transition: .25s ease-in-out 0s; 25 | } 26 | &:active:after { 27 | content: "copied!"; 28 | background-color: #787878; 29 | } 30 | &:hover:after { 31 | visibility: visible; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Copy/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | // import classNames from 'classnames' 4 | import styles from './Copy.css' 5 | 6 | /* include non SSR compatible library */ 7 | const Clipboard = (typeof window !== 'undefined') ? require('clipboard') : null 8 | 9 | const propTypes = { 10 | /** Text to copy */ 11 | text: PropTypes.string, 12 | element: PropTypes.element, 13 | /** Component rendered that when clicked copies the text */ 14 | children: PropTypes.any, 15 | /** Custom function to run on Copy */ 16 | onCopy: PropTypes.func, 17 | } 18 | 19 | export default class Copy extends React.Component { 20 | constructor(props, context) { 21 | super(props, context) 22 | this.clipboardInstance = null 23 | } 24 | componentDidMount() { 25 | const { onCopy, element } = this.props 26 | if (React.isValidElement(element)) { 27 | this.clipboardInstance = new Clipboard(this.copyElement, { 28 | text: () => element.props.value 29 | }) 30 | } else { 31 | this.clipboardInstance = new Clipboard(this.copyElement) 32 | } 33 | this.clipboardInstance.on('success', (e) => { 34 | if (onCopy && typeof onCopy === 'function') { 35 | onCopy(e) 36 | } 37 | e.clearSelection() 38 | }) 39 | 40 | this.clipboardInstance.on('error', (e) => { 41 | console.error('Action:', e.action) 42 | console.error('Trigger:', e.trigger) 43 | }) 44 | } 45 | componentWillUnmount() { 46 | this.clipboardInstance.destroy() 47 | } 48 | render() { 49 | const { text, children, className } = this.props 50 | let copyText 51 | if (text) { 52 | copyText = text 53 | } else if (children && typeof children === 'string') { 54 | copyText = children 55 | } 56 | return ( 57 | { this.copyElement = c }} 60 | data-clipboard-text={copyText} 61 | > 62 | {children} 63 | 64 | ) 65 | } 66 | } 67 | 68 | Copy.propTypes = propTypes 69 | -------------------------------------------------------------------------------- /src/components/Disqus/Disqus.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDisqusComments from 'react-disqus-comments' 3 | import urljoin from 'url-join' 4 | import config from '../../../_site-config' 5 | 6 | class Disqus extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | toasts: [] 11 | } 12 | this.notifyAboutComment = this.notifyAboutComment.bind(this) 13 | this.onSnackbarDismiss = this.onSnackbarDismiss.bind(this) 14 | } 15 | 16 | onSnackbarDismiss() { 17 | const [, ...toasts] = this.state.toasts 18 | this.setState({ toasts }) 19 | } 20 | notifyAboutComment() { 21 | const toasts = this.state.toasts.slice() 22 | toasts.push({ text: 'New comment available!' }) 23 | this.setState({ toasts }) 24 | } 25 | render() { 26 | const { postNode } = this.props 27 | if (!config.disqusShortname) { 28 | return null 29 | } 30 | const post = postNode.frontmatter 31 | const url = urljoin( 32 | config.siteUrl, 33 | config.pathPrefix, 34 | // postNode.fields.slug 35 | ) 36 | return ( 37 | null 38 | ) 39 | } 40 | } 41 | 42 | export default Disqus 43 | -------------------------------------------------------------------------------- /src/components/FieldSet/Fieldset.css: -------------------------------------------------------------------------------- 1 | .fieldSet { 2 | display: flex; 3 | margin-bottom: 20px; 4 | label { 5 | user-select: none; 6 | } 7 | } 8 | .horizontal { 9 | align-items: center; 10 | :global(.component-label) { 11 | margin-bottom: 0px; 12 | } 13 | } 14 | 15 | .vertical { 16 | flex-direction: column; 17 | } 18 | 19 | .labelWrapper { 20 | font-size: 12px; 21 | font-weight: 500; 22 | color: #9e9e9e; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/FieldSet/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import styles from './Fieldset.css' 5 | 6 | const propTypes = { 7 | /** Custom CSS Classes */ 8 | className: PropTypes.string, 9 | /** Alignment of label + input */ 10 | align: PropTypes.oneOf(['horizontal', 'vertical']), 11 | /** Style of Label Container */ 12 | labelStyle: PropTypes.object, 13 | /** Style of Input Container */ 14 | inputStyle: PropTypes.object, 15 | children: PropTypes.any, 16 | } 17 | 18 | const defaultProps = { 19 | align: 'vertical' 20 | } 21 | 22 | /** 23 | * Used to group `Label` and `Input` components. Must have exactly 2 children 24 | */ 25 | const FieldSet = (props) => { 26 | const { 27 | children, 28 | className, 29 | align, 30 | labelStyle, 31 | inputStyle, 32 | ...other 33 | } = props 34 | const classes = classNames( 35 | 'component-fieldSet', 36 | styles.fieldSet, 37 | className, 38 | styles[align] 39 | // propBasedClasses 40 | ) 41 | 42 | if (!children || children.length !== 2) { 43 | throw new Error('FieldSet component must have exactly 2 children') 44 | } 45 | 46 | return ( 47 |
48 |
49 | {children[0]} 50 |
51 |
52 | {children[1]} 53 |
54 |
55 | ) 56 | } 57 | 58 | FieldSet.propTypes = propTypes 59 | FieldSet.defaultProps = defaultProps 60 | 61 | export default FieldSet 62 | -------------------------------------------------------------------------------- /src/components/Form/AutoForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import getFormData from 'get-form-data' 3 | 4 | const { getFieldData } = getFormData 5 | 6 | export default class AutoForm extends React.Component { 7 | static defaultProps = { 8 | component: 'form', 9 | trim: false, 10 | trimOnSubmit: false, 11 | } 12 | 13 | _onChange = (e) => { 14 | let { form, name } = e.target 15 | let data = getFieldData(form, name, { trim: this.props.trim }) 16 | let change = {} 17 | change[name] = data 18 | this.props.onChange(e, name, data, change) 19 | } 20 | 21 | _onSubmit = (e) => { 22 | let data = getFormData(e.target, {trim: this.props.trimOnSubmit || this.props.trim}) 23 | this.props.onSubmit(e, data) 24 | } 25 | 26 | render() { 27 | let { 28 | children, component: Component, onChange, onSubmit, 29 | trim, trimOnSubmit, // eslint-disable-line no-unused-vars 30 | ...props 31 | } = this.props 32 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Form/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AutoForm from './AutoForm' 3 | 4 | 5 | function fixScroll(){ 6 | // http://bit.ly/2gQ6jFJ 7 | this.scrollIntoView(false) 8 | } 9 | 10 | export default class Form extends React.Component { 11 | componentDidMount() { 12 | const list = this.node.querySelectorAll('input,select,textarea') 13 | for (var i = 0; i < list.length; i++) { 14 | list[i].addEventListener('invalid', fixScroll) 15 | } 16 | } 17 | componentWillUnmount() { 18 | const list = this.node.querySelectorAll('input,select,textarea') 19 | for (var i = 0; i < list.length; i++) { 20 | list[i].removeEventListener('invalid', fixScroll) 21 | } 22 | } 23 | 24 | handleSubmit = (event, data) => { 25 | const { onSubmit, id, onChange, trimOnSubmit, children } = this.props 26 | event.preventDefault() 27 | if (onSubmit) { 28 | onSubmit(event, data) 29 | } 30 | } 31 | 32 | render() { 33 | const { onSubmit, id, onChange, trimOnSubmit, children, name } = this.props 34 | return ( 35 |
{ this.node = node }}> 36 | 37 | {children} 38 | 39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/GithubCorner/GithubCorner.css: -------------------------------------------------------------------------------- 1 | .githubCorner svg { 2 | height: 80px; 3 | width: 80px; 4 | fill: #707070; 5 | color: #fff; 6 | position: absolute; 7 | top: 0; 8 | border: 0; 9 | right: 0; 10 | z-index: 99; 11 | transition: fill 0.5s ease; 12 | } 13 | .githubCorner:hover svg { 14 | fill: #43433e; 15 | } 16 | .githubCorner:hover .octoArm { 17 | animation: octocat-wave 560ms ease-in-out; 18 | } 19 | 20 | @keyframes octocat-wave { 21 | 0%, 100%{ 22 | transform: rotate(0) 23 | } 24 | 20%, 60%{ 25 | transform: rotate(-25deg) 26 | } 27 | 40%, 80%{ 28 | transform: rotate(10deg) 29 | } 30 | } 31 | 32 | @media (max-width: 768px) { 33 | .githubCorner svg { 34 | height: 60px; 35 | width: 60px; 36 | } 37 | } 38 | 39 | @media (max-width:500px) { 40 | .githubCorner:hover .octoArm { 41 | animation:none 42 | } 43 | .githubCorner .octoArm { 44 | animation: octocat-wave 560ms ease-in-out 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './GithubCorner.css' 3 | 4 | const GitHubCorner = ({ url }) => { 5 | return ( 6 | 7 | 12 | 13 | ) 14 | } 15 | 16 | export default GitHubCorner 17 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.css: -------------------------------------------------------------------------------- 1 | 2 | .wrapper { 3 | display: inline-block; 4 | fill: inherit; 5 | position: relative; 6 | } 7 | .icon { 8 | display: flex; 9 | width: $iconDefault; 10 | height: $iconDefault; 11 | &.spinning { 12 | animation: spin 1s infinite linear; 13 | } 14 | } 15 | .hasClick { 16 | cursor: pointer; 17 | } 18 | 19 | @keyframes spin { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import sprite from '../../icons/sprite' 5 | import addSVGtoDOM from './utils/addSvgToDom' 6 | import variables from '../../_variables' 7 | import styles from './Icon.css' 8 | 9 | const propTypes = { 10 | /** Custom CSS Classes */ 11 | className: PropTypes.string, 12 | /** Custom CSS Classes */ 13 | style: PropTypes.object, 14 | /** Size of Icon. Takes number or pixel value. Example size={30} */ 15 | size: PropTypes.oneOfType([ 16 | PropTypes.string, 17 | PropTypes.number 18 | ]), 19 | /** Fill color override from default. Hex value */ 20 | fill: PropTypes.string, 21 | /** if true the icon will spin */ 22 | isSpinning: PropTypes.bool, 23 | /** Custom on click function */ 24 | onClick: PropTypes.func, 25 | /** Optional inline SVG children */ 26 | children: PropTypes.element, 27 | } 28 | 29 | const defaultProps = { 30 | size: variables.iconDefault 31 | } 32 | 33 | /** 34 | * See all icons at Icons list 35 | */ 36 | const Icon = ({children, className, size, onClick, ...props}) => { 37 | const classes = classNames( 38 | 'component-icon', 39 | styles.wrapper, 40 | className 41 | ) 42 | 43 | const svgClasses = classNames( 44 | styles.icon, 45 | { 46 | [styles.spinning]: props.isSpinning, 47 | [styles.hasClick]: onClick 48 | } 49 | ) 50 | 51 | const customSize = { 52 | height: size, 53 | width: size 54 | } 55 | 56 | const fillStyles = (props.fill) ? {fill: props.fill} : {} 57 | 58 | const platformPrefix = '' 59 | 60 | let iconContents = ( 61 | 62 | ) 63 | /* If inline SVG used render */ 64 | if (children && (children.type === 'g' || children.type === 'svg')) { 65 | iconContents = children 66 | } 67 | 68 | const inlinedStyles = { 69 | ...customSize, 70 | ...fillStyles 71 | } 72 | 73 | return ( 74 | 75 | 76 | {iconContents} 77 | 78 | 79 | ) 80 | } 81 | 82 | Icon.propTypes = propTypes 83 | Icon.defaultProps = defaultProps 84 | 85 | // TODO figure out if we want sprite in JS 86 | Icon.loadSprite = () => { 87 | addSVGtoDOM(null, sprite) 88 | } 89 | 90 | export default Icon 91 | -------------------------------------------------------------------------------- /src/components/Icon/utils/addSvgToDom.js: -------------------------------------------------------------------------------- 1 | 2 | export default function addSVGtoDOM(target, sprite) { 3 | let svg = target || document.getElementById('svg-sprite') 4 | if (!svg) { 5 | svg = document.createElementNS(null, 'svg') 6 | svg.setAttribute('width', '0') 7 | svg.setAttribute('height', '0') 8 | svg.setAttribute('style', 'display: none') 9 | // svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink') 10 | 11 | svg.setAttribute('id', 'svg-sprite') 12 | document.body.appendChild(svg) 13 | 14 | const receptacle = document.createElement('div') 15 | const svgfragment = `${sprite}` 16 | receptacle.innerHTML = `${svgfragment}` 17 | Array.prototype.slice.call(receptacle.childNodes[0].childNodes).forEach((el) => { 18 | svg.appendChild(el) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Input/Input.css: -------------------------------------------------------------------------------- 1 | 2 | .inputWrapper { 3 | position: relative; 4 | 5 | &:hover { 6 | .copyIcon { 7 | opacity: 1; 8 | } 9 | } 10 | } 11 | .inputWrapper, .input { 12 | width: 100%; 13 | } 14 | 15 | .input { 16 | font-size: 16px; 17 | padding: 10px; 18 | border: 1px solid #949494; 19 | &:focus { 20 | border: 1px solid #000; 21 | outline: none; 22 | } 23 | &:disabled { 24 | cursor: not-allowed; 25 | background: #e6e6e6; 26 | } 27 | &:read-only { 28 | cursor: pointer; 29 | } 30 | &.hasIcon { 31 | padding-left: 30px; 32 | } 33 | /* Kinds */ 34 | &.transparent { 35 | color: #fff; 36 | background-color: transparent; 37 | border: 1px solid transparent; 38 | } 39 | &.default { 40 | border-radius: 2px; 41 | border: none; 42 | font-size: 16px; 43 | padding: 11px 15px; 44 | min-width: 300px; 45 | display: inline-block; 46 | box-shadow: 0px 0px 0px 2px rgba(120, 130, 152, 0.25); 47 | border: none; 48 | outline: none; 49 | transition: all 0.3s ease; 50 | &.valid { 51 | color: $primary; 52 | } 53 | &:hover, &:active, &:focus { 54 | box-shadow: 0px 0px 0px 2px $primary; 55 | } 56 | &.invalid { 57 | box-shadow: 0px 0px 0px 2px rgba(216, 0, 50, 0.54); 58 | } 59 | /* Placeholder color. Must be separated ¯\_(ツ)_/¯ */ 60 | &::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 61 | color: $grey; 62 | } 63 | &::-moz-placeholder { /* Firefox 19+ */ 64 | color: $grey; 65 | } 66 | &:-ms-input-placeholder { /* IE 10+ */ 67 | color: $grey; 68 | } 69 | &:-moz-placeholder { /* Firefox 18- */ 70 | color: $grey; 71 | } 72 | } 73 | &.otherFormKind { 74 | color: #000; 75 | } 76 | } 77 | 78 | textarea.input { 79 | padding: 13px 10px; 80 | } 81 | 82 | .iconWrapper { 83 | position: absolute; 84 | top: 0px; 85 | height: 100%; 86 | display: flex; 87 | align-items: center; 88 | } 89 | .icon {} 90 | 91 | .validation { 92 | position: absolute; 93 | height: 100%; 94 | width: 100%; 95 | display: flex; 96 | align-items: center; 97 | justify-content: flex-end; 98 | top: -33px; 99 | font-size: 1.3rem; 100 | user-select: none; 101 | cursor: pointer; 102 | pointer-events: none; 103 | } 104 | 105 | .copyIcon { 106 | position: absolute; 107 | opacity: 0.2; 108 | cursor: pointer; 109 | user-select: none; 110 | right: -2em; 111 | top: 0.5em; 112 | transition: opacity 500ms; 113 | } 114 | 115 | @media (max-width: 768px) { 116 | .validation { 117 | font-size: 11px; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import formValidation from './utils/validation' 5 | import Icon from '../Icon' 6 | import Copy from '../Copy' 7 | import styles from './Input.css' 8 | 9 | const propTypes = { 10 | /** Custom CSS Classes */ 11 | className: PropTypes.string, 12 | /** Placeholder text */ 13 | placeholder: PropTypes.string, 14 | /** Set type of HTML5 input */ 15 | type: PropTypes.string, 16 | /** Set current value */ 17 | value: PropTypes.string, 18 | /** visual styles */ 19 | kind: PropTypes.oneOf(['default']), 20 | /** disable input field if true */ 21 | isDisabled: PropTypes.bool, 22 | /** make field required */ 23 | isRequired: PropTypes.bool, 24 | /** add icon into field */ 25 | icon: PropTypes.oneOfType([ 26 | PropTypes.string, 27 | PropTypes.element 28 | ]), 29 | /** Size of Icon. Takes number or pixel value. Example size={30} */ 30 | iconSize: PropTypes.oneOfType([ 31 | PropTypes.string, 32 | PropTypes.number 33 | ]), 34 | /** Show copy icon: Enables or disables a copy helper icon next to the input */ 35 | isCopyable: PropTypes.bool, 36 | /** field validation. Can be regex, function, or keyName from validation utils */ 37 | validation: PropTypes.oneOfType([ 38 | PropTypes.string, 39 | PropTypes.func, 40 | PropTypes.object 41 | ]), 42 | /** Custom Classname when input valid */ 43 | validClassName: PropTypes.string, 44 | /** Custom Classname when input invalid */ 45 | invalidClassName: PropTypes.string, 46 | /** Custom Error text */ 47 | errorMessage: PropTypes.string, 48 | /** Custom Error text CSS class */ 49 | errorMessageClassName: PropTypes.string, 50 | /** Run function onBlur */ 51 | onBlur: PropTypes.func, 52 | /** Run function onChange */ 53 | onChange: PropTypes.func, 54 | /** Run function onFocus */ 55 | onFocus: PropTypes.func, 56 | /** Run function onKeyPress */ 57 | onKeyPress: PropTypes.func, 58 | /** debounce validation timeout */ 59 | debounce: PropTypes.number, 60 | /** Make textarea instead of input */ 61 | isTextArea: PropTypes.bool, 62 | /** (dont use unless necessry) Set to use outside state and disable debounce */ 63 | isControlled: PropTypes.bool, 64 | } 65 | 66 | const defaultProps = { 67 | isDisabled: false, 68 | isRequired: false, 69 | type: 'text', 70 | kind: 'default', 71 | debounce: 1000, 72 | iconSize: 25, 73 | isCopyable: false, 74 | validClassName: styles.valid, 75 | invalidClassName: styles.invalid 76 | } 77 | 78 | /** 79 | * See all `components/Input/utils/validation` for prebaked validation 80 | */ 81 | class Input extends Component { 82 | constructor(props, context) { 83 | super(props, context) 84 | this.state = { 85 | isValid: this.doValidation(props.value).isValid, 86 | blurRanOnce: (props.value) ? true : false, 87 | // Timeout ID 88 | tid: void 0, // eslint-disable-line 89 | } 90 | } 91 | componentDidMount() { 92 | setTimeout(() => { 93 | // sometimes value is set via the DOM. This updates initial state 94 | if (this.textInput) { 95 | const value = this.textInput.value 96 | if (value) { 97 | const inputData = this.doValidation(value) 98 | this.setState({ 99 | tid: void 0, // eslint-disable-line 100 | isValid: inputData.isValid, 101 | errorMessage: inputData.errorMessage, 102 | value 103 | }, this.doVisibleValidation(inputData)) 104 | } 105 | } 106 | }, 0) 107 | } 108 | shouldComponentUpdate(nextProps, nextState) { 109 | const keys = Object.keys(nextProps) 110 | const { value, isValid } = this.state 111 | 112 | // if value invalid, always update 113 | if (!isValid) { 114 | return true 115 | } 116 | 117 | // We only consider the search term from the state 118 | if (value !== nextState.value) { 119 | return true 120 | } 121 | // We render if anything in the properties changed 122 | // > Different number of properties 123 | if (keys.length !== Object.keys(this.props).length) { 124 | return true 125 | } 126 | 127 | // > Different properties 128 | const changed = keys.some(key => nextProps[key] !== this.props[key]) 129 | 130 | if (changed) { 131 | return true 132 | } 133 | 134 | return false 135 | } 136 | componentWillUnmount() { 137 | const { tid } = this.state 138 | window.clearTimeout(tid) 139 | } 140 | doValidation(value) { 141 | const { validation, errorMessage } = this.props 142 | const defaultMessage = errorMessage || 'Invalid Value' 143 | // console.log('validation', validation) 144 | // console.log('do validation', value) 145 | // console.log('formValidation', formValidation) 146 | if (typeof validation === 'string' && formValidation[validation]) { 147 | // check pattern in validations formValidation obj 148 | return { 149 | value: value, 150 | isValid: formValidation[validation].pattern.test(value), 151 | errorMessage: formValidation[validation].message || defaultMessage 152 | } 153 | } else if (typeof validation === 'object' && validation.pattern) { 154 | // if validation object is used 155 | return { 156 | value: value, 157 | isValid: validation.pattern.test(value), 158 | errorMessage: validation.message || defaultMessage 159 | } 160 | } else if (validation instanceof RegExp) { 161 | // check regex passed in 162 | return { 163 | value: value, 164 | isValid: validation.test(value), 165 | errorMessage: errorMessage || defaultMessage 166 | } 167 | } else if (typeof validation === 'function') { 168 | // do custom function for validation 169 | return { 170 | value: value, 171 | isValid: validation(value), 172 | errorMessage: errorMessage || defaultMessage 173 | } 174 | } 175 | // default field is valid if no validation 176 | return { 177 | value: value, 178 | isValid: true, 179 | errorMessage: '' 180 | } 181 | } 182 | handleChange = (event) => { 183 | const { tid } = this.state 184 | const { debounce, isControlled, validation } = this.props 185 | 186 | // If has validation, apply debouncer 187 | let deboundTimeout = (validation) ? debounce : 0 188 | 189 | // If form values controlled by outside state ignore debounce 190 | if (isControlled) { 191 | deboundTimeout = 0 192 | } 193 | 194 | if (tid) { 195 | clearTimeout(tid) 196 | } 197 | 198 | this.setState({ 199 | value: event.target.value, 200 | tid: setTimeout(this.emitDelayedChange, deboundTimeout), 201 | }) 202 | } 203 | emitDelayedChange = () => { 204 | const { value } = this.state 205 | const { onChange } = this.props 206 | 207 | const inputData = this.doValidation(value) 208 | 209 | this.setState({ 210 | tid: void 0, // eslint-disable-line 211 | isValid: inputData.isValid, 212 | errorMessage: inputData.errorMessage 213 | }, this.doVisibleValidation(inputData)) 214 | 215 | if (onChange) { 216 | // because debounce, fake event is passed back 217 | const fakeEvent = {} 218 | fakeEvent.target = this.textInput 219 | onChange(fakeEvent, value, inputData.isValid) 220 | } 221 | } 222 | doVisibleValidation(inputData) { 223 | const { validation, validClassName, invalidClassName } = this.props 224 | const { isValid, value } = inputData 225 | if (validation && !isValid) { 226 | // has validation and is not valid! 227 | if (this.textInput.value) { 228 | this.textInput.classList.remove(validClassName) 229 | this.textInput.classList.add(invalidClassName) 230 | // set fake blur so validation will show 231 | this.setFakeBlur() 232 | // show error message 233 | this.prompt() 234 | } 235 | } else if (validation && isValid) { 236 | // has validation and is valid! 237 | this.textInput.classList.remove(invalidClassName) 238 | this.textInput.classList.add(validClassName) 239 | } 240 | // If input is empty and the validation is bad, remove the valid class 241 | if (!value && !isValid) { 242 | this.textInput.classList.remove(validClassName) 243 | } 244 | } 245 | setFakeBlur = () => { 246 | this.setState({ 247 | blurRanOnce: true 248 | }) 249 | } 250 | handleFocus = (event) => { 251 | const { onFocus, readOnly } = this.props 252 | const { isValid } = this.state 253 | 254 | if (readOnly) { 255 | this.select() 256 | } 257 | 258 | // this.outlineInput() 259 | if (onFocus) { 260 | onFocus(event, event.target.value, isValid) 261 | } 262 | } 263 | prompt = (cb) => { 264 | const { value } = this.state 265 | const { onChange } = this.props 266 | 267 | const inputData = this.doValidation(value) 268 | 269 | this.setState({ 270 | tid: void 0, // eslint-disable-line 271 | isValid: inputData.isValid, 272 | errorMessage: inputData.errorMessage 273 | }, () => { 274 | if (cb) { 275 | cb(inputData) 276 | } 277 | }) 278 | } 279 | handleClick = (event) => { 280 | const { onClick } = this.props 281 | if (onClick) { 282 | onClick(event) 283 | } 284 | if (this.textInput.value) { 285 | // make onClick 'trigger' a blur 286 | this.setFakeBlur() 287 | } 288 | } 289 | handleBlur = (event) => { 290 | const { onBlur, validClassName } = this.props 291 | const { isValid } = this.state 292 | if (onBlur) { 293 | onBlur(event, event.target.value, isValid) 294 | } 295 | 296 | if (event.target.value) { 297 | // only show if input has some value 298 | this.prompt((inputData) => { 299 | this.doVisibleValidation(inputData) 300 | }) 301 | } 302 | 303 | if (!event.target.value) { 304 | this.textInput.classList.remove(validClassName) 305 | } 306 | // console.log('this.state.', this.state) 307 | // console.log('this.state.blurRanOnce', this.state.blurRanOnce) 308 | // console.log('event.target.value', event.target.value) 309 | // Set blur state to show validations 310 | if (!this.state.blurRanOnce && event.target.value) { 311 | // capture focus if input wrong 312 | this.setState({ 313 | blurRanOnce: true 314 | }, this.captureFocusWhenInvalid()) 315 | } 316 | } 317 | captureFocusWhenInvalid() { 318 | if (!this.state.isValid) { 319 | // not sure about this guy. Results in different form tabbing behavior 320 | // this.focus() 321 | } 322 | } 323 | showValidation() { 324 | const { isValid, errorMessage, blurRanOnce } = this.state 325 | const { errorMessageClassName } = this.props 326 | if (isValid) { 327 | return null 328 | } else if (blurRanOnce) { 329 | const classes = classNames(styles.validation, errorMessageClassName) 330 | return ( 331 |
332 | {errorMessage} 333 |
334 | ) 335 | } 336 | } 337 | select = () => { 338 | this.textInput.select() 339 | } 340 | blur = () => { 341 | this.textInput.blur() 342 | } 343 | focus = () => { 344 | this.textInput.focus() 345 | } 346 | render() { 347 | const { 348 | className, 349 | isDisabled, 350 | isRequired, 351 | validation, // eslint-disable-line 352 | invalidClassName, // eslint-disable-line 353 | validClassName, // eslint-disable-line 354 | errorMessage, 355 | errorMessageClassName, // eslint-disable-line 356 | debounce, // eslint-disable-line 357 | type, 358 | value, 359 | kind, 360 | icon, 361 | isTextArea, 362 | iconSize, 363 | isCopyable, 364 | ...others 365 | } = this.props 366 | 367 | const { isValid } = this.state 368 | const classes = classNames( 369 | className, 370 | styles.input, 371 | styles[kind], 372 | { 373 | [styles.hasIcon]: icon 374 | } 375 | ) 376 | // console.log('isValid', isValid) 377 | // console.log('errorMessage', this.state.errorMessage) 378 | 379 | const props = { 380 | ...others, 381 | onChange: this.handleChange, 382 | onBlur: this.handleBlur, 383 | onFocus: this.handleFocus, 384 | onClick: this.handleClick, 385 | ref: (input) => { this.textInput = input }, 386 | role: 'input', 387 | name: others.name || others.id || others.ref || formatName(others.placeholder), 388 | disabled: isDisabled, 389 | required: isRequired, 390 | type, 391 | value, 392 | className: classes, 393 | } 394 | 395 | let iconRender 396 | if (icon) { 397 | iconRender = ( 398 |
399 | 400 |
401 | ) 402 | } 403 | 404 | let tag = 405 | 406 | if (isTextArea) { 407 | tag =