Initial commit from gatsby: (https://github.com/netlify-templates/gatsby-starter-netlify-cms.git)
14
.dependabot/config.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
version: 1
|
||||
|
||||
update_configs:
|
||||
- package_manager: javascript
|
||||
directory: /
|
||||
update_schedule: live
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: security
|
||||
automerged_updates:
|
||||
- match:
|
||||
dependency_type: all
|
||||
update_type: in_range
|
||||
version_requirement_updates: widen_ranges
|
||||
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
<!-- Please don't delete this template because we'll close your issue -->
|
||||
<!-- Before creating an issue please make sure you are using the latest version of the starter. -->
|
||||
<!-- This project is starter project using Gatsby and NetlifyCMS in it, if you think the issue can originate from upstream then please report it-->
|
||||
# Bug report
|
||||
|
||||
<!-- Please ask questions on Spectrum for Gatsby questions or the Gitter channel for NetlifyCMS. -->
|
||||
<!-- https://spectrum.chat/?t=da07ec65-96f9-41be-baf0-0271b5b772ef -->
|
||||
<!-- https://gitter.im/netlify/NetlifyCMS -->
|
||||
<!-- Issues which contain questions or support requests will be closed. -->
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
|
||||
**If the current behavior is a bug, please provide the steps to reproduce.**
|
||||
|
||||
|
||||
<!-- A great way to do this is to provide your configuration via a GitHub repository -->
|
||||
<!-- The most helpful is a minimal reproduction with instructions on how to reproduce -->
|
||||
<!-- Please only add small code snippets directly into this issue -->
|
||||
<!-- https://gist.github.com is a good place for longer code snippets -->
|
||||
<!-- If your issue is caused by a plugin or loader, please create an issue on the loader/plugin repository instead -->
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
|
||||
<!-- "It should work" is not a helpful explanation -->
|
||||
<!-- Explain exactly how it should behave -->
|
||||
|
||||
**Other relevant information:**
|
||||
|
||||
<!--Run `gatsby info --clipboard` in your project directory and paste the output here. Not working? You may need to update your global gatsby-cli - `npm install -g gatsby-cli` -->
|
||||
|
||||
Node.js version:
|
||||
NPM/Yarn version
|
||||
Operating System:
|
||||
Additional tools:
|
||||
18
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Other
|
||||
about: Something else
|
||||
|
||||
---
|
||||
|
||||
<!-- Bug reports and Feature requests must use other templates, or will be closed -->
|
||||
<!-- Please ask questions on the NetlifyCMS Gitter channel (https://gitter.im/netlify/NetlifyCMS). -->
|
||||
<!-- Issues which contain questions or support requests will be closed. -->
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
<!-- Please don't delete this template or we'll close your issue -->
|
||||
|
||||
## Feature request
|
||||
|
||||
<!-- Issues which contain questions or support requests will be closed. -->
|
||||
<!-- Before creating an issue please make sure you are using the latest version of webpack. -->
|
||||
<!-- Check if this feature need to be implemented in a plugin or loader instead -->
|
||||
<!-- If yes: file the issue on the plugin/loader repo -->
|
||||
<!-- Features related to the development server should be filed on this repo instead -->
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
|
||||
**What is motivation or use case for adding/changing the behavior?**
|
||||
|
||||
|
||||
**How should this be implemented in your opinion?**
|
||||
|
||||
|
||||
**Are you willing to work on this yourself?**
|
||||
yes
|
||||
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Project dependencies
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
.cache/
|
||||
# Build directory
|
||||
public/
|
||||
static/admin/*.bundle.*
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
76
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at david@netlify.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
103
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# CONTRIBUTING
|
||||
|
||||
Contributions are always welcome, no matter how large or small. Before contributing,
|
||||
please read the [code of conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Setup
|
||||
|
||||
> Install yarn on your system: [https://yarnpkg.com/en/docs/install](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
### Install dependencies
|
||||
|
||||
> Only required on the first run, subsequent runs can use `yarn` to both
|
||||
bootstrap and run the development server using `yarn develop`.
|
||||
Since this starter using the [netlify-dev](https://www.netlify.com/products/dev/#how-it-works), there could be further issues you, please check the [netlify-dev](https://github.com/netlify/netlify-dev) repository for further information and set up questions.
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/netlify-templates/gatsby-starter-netlify-cms
|
||||
$ yarn
|
||||
```
|
||||
|
||||
## Available scripts
|
||||
|
||||
|
||||
### `build`
|
||||
|
||||
Build the static files into the `public` folder, turns lambda functions into a deployable form.
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
### `clean`
|
||||
|
||||
Runs `gatsby clean` command.
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
yarn clean
|
||||
```
|
||||
|
||||
### `netlify dev`
|
||||
|
||||
Starts the netlify dev environment, including the gatsby dev environment.
|
||||
For more infor check the [Netlify Dev Docs](https://github.com/netlify/cli/blob/master/docs/netlify-dev.md)
|
||||
|
||||
```sh
|
||||
netlify dev
|
||||
```
|
||||
|
||||
### `develop` or `start`
|
||||
|
||||
Runs the `clean` script and starts the gatsby develop server using the command `gatsby develop`. We recomend using this command when you don't need Netlify specific features
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
yarn develop
|
||||
```
|
||||
### `test`
|
||||
|
||||
Not implmented yet
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
yarn test
|
||||
```
|
||||
|
||||
### `format`
|
||||
|
||||
Formats code and docs according to our style guidelines using `prettier`
|
||||
|
||||
#### Usage
|
||||
|
||||
```sh
|
||||
yarn format
|
||||
```
|
||||
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We actively welcome your pull requests!
|
||||
|
||||
If you need help with Git or our workflow, please ask on [Gitter.im](https://gitter.im/netlify/NetlifyCMS). We want your contributions even if you're just learning Git. Our maintainers are happy to help!
|
||||
|
||||
Netlify CMS uses the [Forking Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow) + [Feature Branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). Additionally, PR's should be [rebased](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) on master when opened, and again before merging.
|
||||
|
||||
1. Fork the repo.
|
||||
2. Create a branch from `master`. If you're addressing a specific issue, prefix your branch name with the issue number.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Run `yarn test` and ensure the test suite passes. (Not applicable yet)
|
||||
5. Use `yarn format` to format and lint your code.
|
||||
6. PR's must be rebased before merge (feel free to ask for help).
|
||||
7. PR should be reviewed by two maintainers prior to merging.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to the Gatsby - Netlify CMS starter, you agree that your contributions will be licensed
|
||||
under its [MIT license](LICENSE).
|
||||
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 gatsbyjs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
19
PULL_REQUEST_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->
|
||||
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
|
||||
<!-- Try to link to an open issue for more information. -->
|
||||
|
||||
|
||||
<!-- In addition to that please answer these questions: -->
|
||||
|
||||
**What kind of change does this PR introduce?**
|
||||
|
||||
<!-- E.g. a bugfix, feature, refactoring, build related change, etc… -->
|
||||
|
||||
**Does this PR introduce a breaking change?**
|
||||
|
||||
<!-- If this PR introduces a breaking change, please describe the impact and a migration path for existing applications. -->
|
||||
|
||||
**What needs to be documented once your changes are merged?**
|
||||
|
||||
<!-- List all the information that needs to be added to the documentation after merge -->
|
||||
<!-- When your changes are merged you will be asked to contribute this to the documentation -->
|
||||
126
README.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Gatsby + Netlify CMS Starter
|
||||
|
||||
[](https://app.netlify.com/sites/gatsby-starter-netlify-cms-ci/deploys)
|
||||
|
||||
**Note:** This starter uses [Gatsby v2](https://www.gatsbyjs.org/blog/2018-09-17-gatsby-v2/).
|
||||
|
||||
This repo contains an example business website that is built with [Gatsby](https://www.gatsbyjs.org/), and [Netlify CMS](https://www.netlifycms.org): **[Demo Link](https://gatsby-netlify-cms.netlify.com/)**.
|
||||
|
||||
It follows the [JAMstack architecture](https://jamstack.org) by using Git as a single source of truth, and [Netlify](https://www.netlify.com) for continuous deployment, and CDN distribution.
|
||||
|
||||
## Features
|
||||
|
||||
- A simple landing page with blog functionality built with Netlify CMS
|
||||
- Editable Pages: Landing, About, Product, Blog-Collection and Contact page with Netlify Form support
|
||||
- Create Blog posts from Netlify CMS
|
||||
- Tags: Separate page for posts under each tag
|
||||
- Basic directory organization
|
||||
- Uses Bulma for styling, but size is reduced by `purge-css-plugin`
|
||||
- Blazing fast loading times thanks to pre-rendered HTML and automatic chunk loading of JS files
|
||||
- Uses `gatsby-image` with Netlify-CMS preview support
|
||||
- Separate components for everything
|
||||
- Netlify deploy configuration
|
||||
- Netlify function support, see `lambda` folder
|
||||
- Perfect score on Lighthouse for SEO, Accessibility and Performance (wip:PWA)
|
||||
- ..and more
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node (I recommend using v8.2.0 or higher)
|
||||
- [Gatsby CLI](https://www.gatsbyjs.org/docs/)
|
||||
- [Netlify CLI](https://github.com/netlify/cli)
|
||||
|
||||
## Getting Started (Recommended)
|
||||
|
||||
Netlify CMS can run in any frontend web environment, but the quickest way to try it out is by running it on a pre-configured starter site with Netlify. The example here is the Kaldi coffee company template (adapted from [One Click Hugo CMS](https://github.com/netlify-templates/one-click-hugo-cms)). Use the button below to build and deploy your own copy of the repository:
|
||||
|
||||
<a href="https://app.netlify.com/start/deploy?repository=https://github.com/netlify-templates/gatsby-starter-netlify-cms&stack=cms"><img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify"></a>
|
||||
|
||||
After clicking that button, you’ll authenticate with GitHub and choose a repository name. Netlify will then automatically create a repository in your GitHub account with a copy of the files from the template. Next, it will build and deploy the new site on Netlify, bringing you to the site dashboard when the build is complete. Next, you’ll need to set up Netlify’s Identity service to authorize users to log in to the CMS.
|
||||
|
||||
### Access Locally
|
||||
|
||||
Pulldown a local copy of the Github repository Netlify created for you, with the name you specified in the previous step
|
||||
```
|
||||
$ git clone https://github.com/[GITHUB_USERNAME]/[REPO_NAME].git
|
||||
$ cd [REPO_NAME]
|
||||
$ yarn
|
||||
$ netlify dev # or ntl dev
|
||||
```
|
||||
|
||||
This uses the new [Netlify Dev](https://www.netlify.com/products/dev/?utm_source=blog&utm_medium=netlifycms&utm_campaign=devex) CLI feature to serve any functions you have in the `lambda` folder.
|
||||
|
||||
To test the CMS locally, you'll need to run a production build of the site:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
$ netlify dev # or ntl dev
|
||||
```
|
||||
|
||||
### Media Libraries (installed, but optional)
|
||||
|
||||
Media Libraries have been included in this starter as a default. If you are not planning to use `Uploadcare` or `Cloudinary` in your project, you **can** remove them from module import and registration in `src/cms/cms.js`. Here is an example of the lines to comment or remove them your project.
|
||||
|
||||
```javascript
|
||||
import CMS from 'netlify-cms-app'
|
||||
// import uploadcare from 'netlify-cms-media-library-uploadcare'
|
||||
// import cloudinary from 'netlify-cms-media-library-cloudinary'
|
||||
|
||||
import AboutPagePreview from './preview-templates/AboutPagePreview'
|
||||
import BlogPostPreview from './preview-templates/BlogPostPreview'
|
||||
import ProductPagePreview from './preview-templates/ProductPagePreview'
|
||||
import IndexPagePreview from './preview-templates/IndexPagePreview'
|
||||
|
||||
// CMS.registerMediaLibrary(uploadcare);
|
||||
// CMS.registerMediaLibrary(cloudinary);
|
||||
|
||||
CMS.registerPreviewTemplate('index', IndexPagePreview)
|
||||
CMS.registerPreviewTemplate('about', AboutPagePreview)
|
||||
CMS.registerPreviewTemplate('products', ProductPagePreview)
|
||||
CMS.registerPreviewTemplate('blog', BlogPostPreview)
|
||||
```
|
||||
|
||||
Note: Don't forget to also remove them from `package.json` and `yarn.lock` / `package-lock.json` using `yarn` or `npm`. During the build netlify-cms-app will bundle the media libraries as well, having them removed will save you build time.
|
||||
Example:
|
||||
```
|
||||
yarn remove netlify-cms-media-library-uploadcare
|
||||
```
|
||||
OR
|
||||
```
|
||||
yarn remove netlify-cms-media-library-cloudinary
|
||||
```
|
||||
## Getting Started (Without Netlify)
|
||||
|
||||
```
|
||||
$ gatsby new [SITE_DIRECTORY_NAME] https://github.com/netlify-templates/gatsby-starter-netlify-cms/
|
||||
$ cd [SITE_DIRECTORY_NAME]
|
||||
$ npm run build
|
||||
$ npm run serve
|
||||
```
|
||||
|
||||
### Setting up the CMS
|
||||
|
||||
Follow the [Netlify CMS Quick Start Guide](https://www.netlifycms.org/docs/quick-start/#authentication) to set up authentication, and hosting.
|
||||
|
||||
## Debugging
|
||||
|
||||
Windows users might encounter `node-gyp` errors when trying to npm install.
|
||||
To resolve, make sure that you have both Python 2.7 and the Visual C++ build environment installed.
|
||||
|
||||
```
|
||||
npm config set python python2.7
|
||||
npm install --global --production windows-build-tools
|
||||
```
|
||||
|
||||
[Full details here](https://www.npmjs.com/package/node-gyp 'NPM node-gyp page')
|
||||
|
||||
MacOS users might also encounter some errors, for more info check [node-gyp](https://github.com/nodejs/node-gyp). We recommend using the latest stable node version.
|
||||
|
||||
## Purgecss
|
||||
|
||||
This plugin uses [gatsby-plugin-purgecss](https://www.gatsbyjs.org/packages/gatsby-plugin-purgecss/) and [bulma](https://bulma.io/). The bulma builds are usually ~170K but reduced 90% by purgecss.
|
||||
|
||||
# CONTRIBUTING
|
||||
|
||||
Contributions are always welcome, no matter how large or small. Before contributing,
|
||||
please read the [code of conduct](CODE_OF_CONDUCT.md).
|
||||
2
_headers
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/static/*
|
||||
Cache-Control: "public, max-age=360000"
|
||||
77
gatsby-config.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: 'Gatsby + Netlify CMS Starter',
|
||||
description:
|
||||
'This repo contains an example business website that is built with Gatsby, and Netlify CMS.It follows the JAMstack architecture by using Git as a single source of truth, and Netlify for continuous deployment, and CDN distribution.',
|
||||
},
|
||||
plugins: [
|
||||
'gatsby-plugin-react-helmet',
|
||||
'gatsby-plugin-sass',
|
||||
{
|
||||
// keep as first gatsby-source-filesystem plugin for gatsby image support
|
||||
resolve: 'gatsby-source-filesystem',
|
||||
options: {
|
||||
path: `${__dirname}/static/img`,
|
||||
name: 'uploads',
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-source-filesystem',
|
||||
options: {
|
||||
path: `${__dirname}/src/pages`,
|
||||
name: 'pages',
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-source-filesystem',
|
||||
options: {
|
||||
path: `${__dirname}/src/img`,
|
||||
name: 'images',
|
||||
},
|
||||
},
|
||||
'gatsby-plugin-sharp',
|
||||
'gatsby-transformer-sharp',
|
||||
{
|
||||
resolve: 'gatsby-transformer-remark',
|
||||
options: {
|
||||
plugins: [
|
||||
{
|
||||
resolve: 'gatsby-remark-relative-images',
|
||||
options: {
|
||||
name: 'uploads',
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-remark-images',
|
||||
options: {
|
||||
// It's important to specify the maxWidth (in pixels) of
|
||||
// the content container as this plugin uses this as the
|
||||
// base for generating different widths of each image.
|
||||
maxWidth: 2048,
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-remark-copy-linked-files',
|
||||
options: {
|
||||
destinationDir: 'static',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-netlify-cms',
|
||||
options: {
|
||||
modulePath: `${__dirname}/src/cms/cms.js`,
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-purgecss', // purges all unused/unreferenced css rules
|
||||
options: {
|
||||
develop: true, // Activates purging in npm run develop
|
||||
purgeOnly: ['/all.sass'], // applies purging only on the bulma css file
|
||||
},
|
||||
}, // must be after other CSS plugins
|
||||
'gatsby-plugin-netlify', // make sure to keep it last in the array
|
||||
],
|
||||
}
|
||||
87
gatsby-node.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const { createFilePath } = require('gatsby-source-filesystem')
|
||||
const { fmImagesToRelative } = require('gatsby-remark-relative-images')
|
||||
|
||||
exports.createPages = ({ actions, graphql }) => {
|
||||
const { createPage } = actions
|
||||
|
||||
return graphql(`
|
||||
{
|
||||
allMarkdownRemark(limit: 1000) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
tags
|
||||
templateKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`).then((result) => {
|
||||
if (result.errors) {
|
||||
result.errors.forEach((e) => console.error(e.toString()))
|
||||
return Promise.reject(result.errors)
|
||||
}
|
||||
|
||||
const posts = result.data.allMarkdownRemark.edges
|
||||
|
||||
posts.forEach((edge) => {
|
||||
const id = edge.node.id
|
||||
createPage({
|
||||
path: edge.node.fields.slug,
|
||||
tags: edge.node.frontmatter.tags,
|
||||
component: path.resolve(
|
||||
`src/templates/${String(edge.node.frontmatter.templateKey)}.js`
|
||||
),
|
||||
// additional data can be passed via context
|
||||
context: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Tag pages:
|
||||
let tags = []
|
||||
// Iterate through each post, putting all found tags into `tags`
|
||||
posts.forEach((edge) => {
|
||||
if (_.get(edge, `node.frontmatter.tags`)) {
|
||||
tags = tags.concat(edge.node.frontmatter.tags)
|
||||
}
|
||||
})
|
||||
// Eliminate duplicate tags
|
||||
tags = _.uniq(tags)
|
||||
|
||||
// Make tag pages
|
||||
tags.forEach((tag) => {
|
||||
const tagPath = `/tags/${_.kebabCase(tag)}/`
|
||||
|
||||
createPage({
|
||||
path: tagPath,
|
||||
component: path.resolve(`src/templates/tags.js`),
|
||||
context: {
|
||||
tag,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.onCreateNode = ({ node, actions, getNode }) => {
|
||||
const { createNodeField } = actions
|
||||
fmImagesToRelative(node) // convert image paths for gatsby images
|
||||
|
||||
if (node.internal.type === `MarkdownRemark`) {
|
||||
const value = createFilePath({ node, getNode })
|
||||
createNodeField({
|
||||
name: `slug`,
|
||||
node,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
17
lambda/hello.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// For more info, check https://docs.netlify.com/functions/build-with-javascript
|
||||
module.exports.handler = async function(event, context) {
|
||||
console.log("queryStringParameters", event.queryStringParameters)
|
||||
return {
|
||||
// return null to show no errors
|
||||
statusCode: 200, // http status code
|
||||
body: JSON.stringify({
|
||||
msg: "Hello, World! This is better " + Math.round(Math.random() * 10)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Now you are ready to access this API from anywhere in your Gatsby app! For example, in any event handler or lifecycle method, insert:
|
||||
// fetch("/.netlify/functions/hello")
|
||||
// .then(response => response.json())
|
||||
// .then(console.log)
|
||||
// For more info see: https://www.gatsbyjs.org/blog/2018-12-17-turning-the-static-dynamic/#static-dynamic-is-a-spectrum
|
||||
8
netlify.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[build]
|
||||
publish = "public"
|
||||
command = "npm run build"
|
||||
functions = "lambda"
|
||||
[build.environment]
|
||||
NODE_VERSION = "12"
|
||||
YARN_VERSION = "1.22.4"
|
||||
YARN_FLAGS = "--no-ignore-optional"
|
||||
23646
package-lock.json
generated
Normal file
50
package.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "gatsby-starter-netlify-cms",
|
||||
"description": "Example Gatsby, and Netlify CMS project",
|
||||
"version": "1.1.3",
|
||||
"author": "Austin Green",
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.0",
|
||||
"gatsby": "^2.20.35",
|
||||
"gatsby-image": "^2.3.5",
|
||||
"gatsby-plugin-netlify": "^2.2.4",
|
||||
"gatsby-plugin-netlify-cms": "^4.2.5",
|
||||
"gatsby-plugin-purgecss": "^5.0.0",
|
||||
"gatsby-plugin-react-helmet": "^3.2.5",
|
||||
"gatsby-plugin-sass": "^2.2.4",
|
||||
"gatsby-plugin-sharp": "^2.5.7",
|
||||
"gatsby-remark-copy-linked-files": "^2.2.4",
|
||||
"gatsby-remark-images": "^3.2.6",
|
||||
"gatsby-remark-relative-images": "^0.3.0",
|
||||
"gatsby-source-filesystem": "^2.2.5",
|
||||
"gatsby-transformer-remark": "^2.7.5",
|
||||
"gatsby-transformer-sharp": "^2.4.7",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash-webpack-plugin": "^0.11.4",
|
||||
"netlify-cms-app": "^2.14.26",
|
||||
"netlify-cms-media-library-cloudinary": "^1.3.10",
|
||||
"netlify-cms-media-library-uploadcare": "^0.5.10",
|
||||
"node-sass": "^4.14.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-helmet": "^6.0.0",
|
||||
"uuid": "^7.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "n/a",
|
||||
"scripts": {
|
||||
"clean": "gatsby clean",
|
||||
"start": "npm run develop",
|
||||
"build": "npm run clean && gatsby build",
|
||||
"develop": "npm run clean && gatsby develop",
|
||||
"format": "prettier --trailing-comma es5 --no-semi --single-quote --write \"{gatsby-*.js,src/**/*.js}\"",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.0.5"
|
||||
}
|
||||
}
|
||||
3
renovate.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["github>netlify/renovate-config:netlify-cms-starter"]
|
||||
}
|
||||
16
src/cms/cms.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import CMS from 'netlify-cms-app'
|
||||
import uploadcare from 'netlify-cms-media-library-uploadcare'
|
||||
import cloudinary from 'netlify-cms-media-library-cloudinary'
|
||||
|
||||
import AboutPagePreview from './preview-templates/AboutPagePreview'
|
||||
import BlogPostPreview from './preview-templates/BlogPostPreview'
|
||||
import ProductPagePreview from './preview-templates/ProductPagePreview'
|
||||
import IndexPagePreview from './preview-templates/IndexPagePreview'
|
||||
|
||||
CMS.registerMediaLibrary(uploadcare)
|
||||
CMS.registerMediaLibrary(cloudinary)
|
||||
|
||||
CMS.registerPreviewTemplate('index', IndexPagePreview)
|
||||
CMS.registerPreviewTemplate('about', AboutPagePreview)
|
||||
CMS.registerPreviewTemplate('products', ProductPagePreview)
|
||||
CMS.registerPreviewTemplate('blog', BlogPostPreview)
|
||||
19
src/cms/preview-templates/AboutPagePreview.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AboutPageTemplate } from '../../templates/about-page'
|
||||
|
||||
const AboutPagePreview = ({ entry, widgetFor }) => (
|
||||
<AboutPageTemplate
|
||||
title={entry.getIn(['data', 'title'])}
|
||||
content={widgetFor('body')}
|
||||
/>
|
||||
)
|
||||
|
||||
AboutPagePreview.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
getIn: PropTypes.func,
|
||||
}),
|
||||
widgetFor: PropTypes.func,
|
||||
}
|
||||
|
||||
export default AboutPagePreview
|
||||
24
src/cms/preview-templates/BlogPostPreview.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { BlogPostTemplate } from '../../templates/blog-post'
|
||||
|
||||
const BlogPostPreview = ({ entry, widgetFor }) => {
|
||||
const tags = entry.getIn(['data', 'tags'])
|
||||
return (
|
||||
<BlogPostTemplate
|
||||
content={widgetFor('body')}
|
||||
description={entry.getIn(['data', 'description'])}
|
||||
tags={tags && tags.toJS()}
|
||||
title={entry.getIn(['data', 'title'])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
BlogPostPreview.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
getIn: PropTypes.func,
|
||||
}),
|
||||
widgetFor: PropTypes.func,
|
||||
}
|
||||
|
||||
export default BlogPostPreview
|
||||
32
src/cms/preview-templates/IndexPagePreview.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { IndexPageTemplate } from '../../templates/index-page'
|
||||
|
||||
const IndexPagePreview = ({ entry, getAsset }) => {
|
||||
const data = entry.getIn(['data']).toJS()
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<IndexPageTemplate
|
||||
image={getAsset(data.image)}
|
||||
title={data.title}
|
||||
heading={data.heading}
|
||||
subheading={data.subheading}
|
||||
description={data.description}
|
||||
intro={data.intro || { blurbs: [] }}
|
||||
mainpitch={data.mainpitch || {}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
}
|
||||
|
||||
IndexPagePreview.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
getIn: PropTypes.func,
|
||||
}),
|
||||
getAsset: PropTypes.func,
|
||||
}
|
||||
|
||||
export default IndexPagePreview
|
||||
56
src/cms/preview-templates/ProductPagePreview.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ProductPageTemplate } from '../../templates/product-page'
|
||||
|
||||
const ProductPagePreview = ({ entry, getAsset }) => {
|
||||
const entryBlurbs = entry.getIn(['data', 'intro', 'blurbs'])
|
||||
const blurbs = entryBlurbs ? entryBlurbs.toJS() : []
|
||||
|
||||
const entryTestimonials = entry.getIn(['data', 'testimonials'])
|
||||
const testimonials = entryTestimonials ? entryTestimonials.toJS() : []
|
||||
|
||||
const entryPricingPlans = entry.getIn(['data', 'pricing', 'plans'])
|
||||
const pricingPlans = entryPricingPlans ? entryPricingPlans.toJS() : []
|
||||
|
||||
return (
|
||||
<ProductPageTemplate
|
||||
image={getAsset(entry.getIn(['data', 'image']))}
|
||||
title={entry.getIn(['data', 'title'])}
|
||||
heading={entry.getIn(['data', 'heading'])}
|
||||
description={entry.getIn(['data', 'description'])}
|
||||
intro={{ blurbs }}
|
||||
main={{
|
||||
heading: entry.getIn(['data', 'main', 'heading']),
|
||||
description: entry.getIn(['data', 'main', 'description']),
|
||||
image1: {
|
||||
image: getAsset(entry.getIn(['data', 'main', 'image1', 'image'])),
|
||||
alt: entry.getIn(['data', 'main', 'image1', 'alt']),
|
||||
},
|
||||
image2: {
|
||||
image: getAsset(entry.getIn(['data', 'main', 'image2', 'image'])),
|
||||
alt: entry.getIn(['data', 'main', 'image2', 'alt']),
|
||||
},
|
||||
image3: {
|
||||
image: getAsset(entry.getIn(['data', 'main', 'image3', 'image'])),
|
||||
alt: entry.getIn(['data', 'main', 'image3', 'alt']),
|
||||
},
|
||||
}}
|
||||
fullImage={entry.getIn(['data', 'full_image'])}
|
||||
testimonials={testimonials}
|
||||
pricing={{
|
||||
heading: entry.getIn(['data', 'pricing', 'heading']),
|
||||
description: entry.getIn(['data', 'pricing', 'description']),
|
||||
plans: pricingPlans,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ProductPagePreview.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
getIn: PropTypes.func,
|
||||
}),
|
||||
getAsset: PropTypes.func,
|
||||
}
|
||||
|
||||
export default ProductPagePreview
|
||||
104
src/components/BlogRoll.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link, graphql, StaticQuery } from 'gatsby'
|
||||
import PreviewCompatibleImage from './PreviewCompatibleImage'
|
||||
|
||||
class BlogRoll extends React.Component {
|
||||
render() {
|
||||
const { data } = this.props
|
||||
const { edges: posts } = data.allMarkdownRemark
|
||||
|
||||
return (
|
||||
<div className="columns is-multiline">
|
||||
{posts &&
|
||||
posts.map(({ node: post }) => (
|
||||
<div className="is-parent column is-6" key={post.id}>
|
||||
<article
|
||||
className={`blog-list-item tile is-child box notification ${
|
||||
post.frontmatter.featuredpost ? 'is-featured' : ''
|
||||
}`}
|
||||
>
|
||||
<header>
|
||||
{post.frontmatter.featuredimage ? (
|
||||
<div className="featured-thumbnail">
|
||||
<PreviewCompatibleImage
|
||||
imageInfo={{
|
||||
image: post.frontmatter.featuredimage,
|
||||
alt: `featured image thumbnail for post ${post.frontmatter.title}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<p className="post-meta">
|
||||
<Link
|
||||
className="title has-text-primary is-size-4"
|
||||
to={post.fields.slug}
|
||||
>
|
||||
{post.frontmatter.title}
|
||||
</Link>
|
||||
<span> • </span>
|
||||
<span className="subtitle is-size-5 is-block">
|
||||
{post.frontmatter.date}
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<p>
|
||||
{post.excerpt}
|
||||
<br />
|
||||
<br />
|
||||
<Link className="button" to={post.fields.slug}>
|
||||
Keep Reading →
|
||||
</Link>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BlogRoll.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
allMarkdownRemark: PropTypes.shape({
|
||||
edges: PropTypes.array,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<StaticQuery
|
||||
query={graphql`
|
||||
query BlogRollQuery {
|
||||
allMarkdownRemark(
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: { frontmatter: { templateKey: { eq: "blog-post" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
excerpt(pruneLength: 400)
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
templateKey
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
featuredpost
|
||||
featuredimage {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 120, quality: 100) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
render={(data, count) => <BlogRoll data={data} count={count} />}
|
||||
/>
|
||||
)
|
||||
19
src/components/Content.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export const HTMLContent = ({ content, className }) => (
|
||||
<div className={className} dangerouslySetInnerHTML={{ __html: content }} />
|
||||
)
|
||||
|
||||
const Content = ({ content, className }) => (
|
||||
<div className={className}>{content}</div>
|
||||
)
|
||||
|
||||
Content.propTypes = {
|
||||
content: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
HTMLContent.propTypes = Content.propTypes
|
||||
|
||||
export default Content
|
||||
36
src/components/Features.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import PreviewCompatibleImage from '../components/PreviewCompatibleImage'
|
||||
|
||||
const FeatureGrid = ({ gridItems }) => (
|
||||
<div className="columns is-multiline">
|
||||
{gridItems.map((item) => (
|
||||
<div key={item.text} className="column is-6">
|
||||
<section className="section">
|
||||
<div className="has-text-centered">
|
||||
<div
|
||||
style={{
|
||||
width: '240px',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
<PreviewCompatibleImage imageInfo={item} />
|
||||
</div>
|
||||
</div>
|
||||
<p>{item.text}</p>
|
||||
</section>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
FeatureGrid.propTypes = {
|
||||
gridItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
image: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
text: PropTypes.string,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default FeatureGrid
|
||||
115
src/components/Footer.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import logo from '../img/logo.svg'
|
||||
import facebook from '../img/social/facebook.svg'
|
||||
import instagram from '../img/social/instagram.svg'
|
||||
import twitter from '../img/social/twitter.svg'
|
||||
import vimeo from '../img/social/vimeo.svg'
|
||||
|
||||
const Footer = class extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<footer className="footer has-background-black has-text-white-ter">
|
||||
<div className="content has-text-centered">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Kaldi"
|
||||
style={{ width: '14em', height: '10em' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="content has-text-centered has-background-black has-text-white-ter">
|
||||
<div className="container has-background-black has-text-white-ter">
|
||||
<div style={{ maxWidth: '100vw' }} className="columns">
|
||||
<div className="column is-4">
|
||||
<section className="menu">
|
||||
<ul className="menu-list">
|
||||
<li>
|
||||
<Link to="/" className="navbar-item">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="navbar-item" to="/about">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="navbar-item" to="/products">
|
||||
Products
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="navbar-item" to="/contact/examples">
|
||||
Form Examples
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="navbar-item"
|
||||
href="/admin/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<section>
|
||||
<ul className="menu-list">
|
||||
<li>
|
||||
<Link className="navbar-item" to="/blog">
|
||||
Latest Stories
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="navbar-item" to="/contact">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div className="column is-4 social">
|
||||
<a title="facebook" href="https://facebook.com">
|
||||
<img
|
||||
src={facebook}
|
||||
alt="Facebook"
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</a>
|
||||
<a title="twitter" href="https://twitter.com">
|
||||
<img
|
||||
className="fas fa-lg"
|
||||
src={twitter}
|
||||
alt="Twitter"
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</a>
|
||||
<a title="instagram" href="https://instagram.com">
|
||||
<img
|
||||
src={instagram}
|
||||
alt="Instagram"
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</a>
|
||||
<a title="vimeo" href="https://vimeo.com">
|
||||
<img
|
||||
src={vimeo}
|
||||
alt="Vimeo"
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer
|
||||
58
src/components/Layout.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import Footer from '../components/Footer'
|
||||
import Navbar from '../components/Navbar'
|
||||
import './all.sass'
|
||||
import useSiteMetadata from './SiteMetadata'
|
||||
import { withPrefix } from 'gatsby'
|
||||
|
||||
const TemplateWrapper = ({ children }) => {
|
||||
const { title, description } = useSiteMetadata()
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<html lang="en" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${withPrefix('/')}img/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={`${withPrefix('/')}img/favicon-32x32.png`}
|
||||
sizes="32x32"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={`${withPrefix('/')}img/favicon-16x16.png`}
|
||||
sizes="16x16"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href={`${withPrefix('/')}img/safari-pinned-tab.svg`}
|
||||
color="#ff4400"
|
||||
/>
|
||||
<meta name="theme-color" content="#fff" />
|
||||
|
||||
<meta property="og:type" content="business.business" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:url" content="/" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`${withPrefix('/')}img/og-image.jpg`}
|
||||
/>
|
||||
</Helmet>
|
||||
<Navbar />
|
||||
<div>{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateWrapper
|
||||
98
src/components/Navbar.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import github from '../img/github-icon.svg'
|
||||
import logo from '../img/logo.svg'
|
||||
|
||||
const Navbar = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
active: false,
|
||||
navBarActiveClass: '',
|
||||
}
|
||||
}
|
||||
|
||||
toggleHamburger = () => {
|
||||
// toggle the active boolean in the state
|
||||
this.setState(
|
||||
{
|
||||
active: !this.state.active,
|
||||
},
|
||||
// after state has been updated,
|
||||
() => {
|
||||
// set the class in state for the navbar accordingly
|
||||
this.state.active
|
||||
? this.setState({
|
||||
navBarActiveClass: 'is-active',
|
||||
})
|
||||
: this.setState({
|
||||
navBarActiveClass: '',
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav
|
||||
className="navbar is-transparent"
|
||||
role="navigation"
|
||||
aria-label="main-navigation"
|
||||
>
|
||||
<div className="container">
|
||||
<div className="navbar-brand">
|
||||
<Link to="/" className="navbar-item" title="Logo">
|
||||
<img src={logo} alt="Kaldi" style={{ width: '88px' }} />
|
||||
</Link>
|
||||
{/* Hamburger menu */}
|
||||
<div
|
||||
className={`navbar-burger burger ${this.state.navBarActiveClass}`}
|
||||
data-target="navMenu"
|
||||
onClick={() => this.toggleHamburger()}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="navMenu"
|
||||
className={`navbar-menu ${this.state.navBarActiveClass}`}
|
||||
>
|
||||
<div className="navbar-start has-text-centered">
|
||||
<Link className="navbar-item" to="/about">
|
||||
About
|
||||
</Link>
|
||||
<Link className="navbar-item" to="/products">
|
||||
Products
|
||||
</Link>
|
||||
<Link className="navbar-item" to="/blog">
|
||||
Blog
|
||||
</Link>
|
||||
<Link className="navbar-item" to="/contact">
|
||||
Contact
|
||||
</Link>
|
||||
<Link className="navbar-item" to="/contact/examples">
|
||||
Form Examples
|
||||
</Link>
|
||||
</div>
|
||||
<div className="navbar-end has-text-centered">
|
||||
<a
|
||||
className="navbar-item"
|
||||
href="https://github.com/netlify-templates/gatsby-starter-netlify-cms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="icon">
|
||||
<img src={github} alt="Github" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
34
src/components/PreviewCompatibleImage.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Img from 'gatsby-image'
|
||||
|
||||
const PreviewCompatibleImage = ({ imageInfo }) => {
|
||||
const imageStyle = { borderRadius: '5px' }
|
||||
const { alt = '', childImageSharp, image } = imageInfo
|
||||
|
||||
if (!!image && !!image.childImageSharp) {
|
||||
return (
|
||||
<Img style={imageStyle} fluid={image.childImageSharp.fluid} alt={alt} />
|
||||
)
|
||||
}
|
||||
|
||||
if (!!childImageSharp) {
|
||||
return <Img style={imageStyle} fluid={childImageSharp.fluid} alt={alt} />
|
||||
}
|
||||
|
||||
if (!!image && typeof image === 'string')
|
||||
return <img style={imageStyle} src={image} alt={alt} />
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
PreviewCompatibleImage.propTypes = {
|
||||
imageInfo: PropTypes.shape({
|
||||
alt: PropTypes.string,
|
||||
childImageSharp: PropTypes.object,
|
||||
image: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
style: PropTypes.object,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default PreviewCompatibleImage
|
||||
40
src/components/Pricing.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const Pricing = ({ data }) => (
|
||||
<div className="columns">
|
||||
{data.map((price) => (
|
||||
<div key={price.plan} className="column">
|
||||
<section className="section">
|
||||
<h4 className="has-text-centered has-text-weight-semibold">
|
||||
{price.plan}
|
||||
</h4>
|
||||
<h2 className="is-size-1 has-text-weight-bold has-text-primary has-text-centered">
|
||||
${price.price}
|
||||
</h2>
|
||||
<p className="has-text-weight-semibold">{price.description}</p>
|
||||
<ul>
|
||||
{price.items.map((item) => (
|
||||
<li key={item} className="is-size-5">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
Pricing.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
plan: PropTypes.string,
|
||||
price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
description: PropTypes.string,
|
||||
items: PropTypes.array,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default Pricing
|
||||
19
src/components/SiteMetadata.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
|
||||
const useSiteMetadata = () => {
|
||||
const { site } = useStaticQuery(
|
||||
graphql`
|
||||
query SITE_METADATA_QUERY {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
return site.siteMetadata
|
||||
}
|
||||
|
||||
export default useSiteMetadata
|
||||
28
src/components/Testimonials.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
const Testimonials = ({ testimonials }) => (
|
||||
<div>
|
||||
{testimonials.map((testimonial) => (
|
||||
<article key={v4()} className="message">
|
||||
<div className="message-body">
|
||||
{testimonial.quote}
|
||||
<br />
|
||||
<cite> – {testimonial.author}</cite>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
Testimonials.propTypes = {
|
||||
testimonials: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
quote: PropTypes.string,
|
||||
author: PropTypes.string,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default Testimonials
|
||||
140
src/components/all.sass
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
@import "~bulma/sass/utilities/initial-variables"
|
||||
|
||||
// Config vars
|
||||
// Kaldi logo brand color: #FD461E
|
||||
$kaldi-red: #D64000
|
||||
$kaldi-red-invert: #fff
|
||||
|
||||
$primary: $kaldi-red
|
||||
$primary-invert: $kaldi-red-invert
|
||||
$body-color: #333
|
||||
$black: #2b2523
|
||||
|
||||
.navbar .navbar-menu
|
||||
box-shadow:none !important
|
||||
|
||||
.content .taglist
|
||||
list-style: none
|
||||
margin-bottom: 0
|
||||
margin-left: 0
|
||||
margin-right: 1.5rem
|
||||
margin-top: 1.5rem
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: left
|
||||
align-items: center
|
||||
li
|
||||
padding: 0 2rem 1rem 0
|
||||
margin-bottom: 1.5rem
|
||||
margin-top: 0
|
||||
|
||||
// Helper Classes
|
||||
.full-width-image-container
|
||||
width: 100vw
|
||||
height: 400px
|
||||
position: relative
|
||||
left: 50%
|
||||
right: 50%
|
||||
margin: 2em -50vw
|
||||
background-size: cover
|
||||
background-position: bottom
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
.full-width-image
|
||||
width: 100vw
|
||||
height: 400px
|
||||
background-size: cover
|
||||
background-position: bottom
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
.btn
|
||||
display: inline-block
|
||||
padding: 12px 16px 10px
|
||||
font-size: 18px
|
||||
font-size: 1rem
|
||||
line-height: 1.25
|
||||
background-color: #fff
|
||||
border-radius: .25rem
|
||||
text-decoration: none
|
||||
font-weight: 700
|
||||
color: #CC3700
|
||||
text-align: center
|
||||
-webkit-box-shadow: inset 0 0 0 2px #CC3700
|
||||
box-shadow: inset 0 0 0 2px #f40
|
||||
-webkit-transition: all .15s ease
|
||||
transition: all .15s ease
|
||||
|
||||
.margin-top-0
|
||||
margin-top: 0 !important
|
||||
|
||||
.navbar-item .icon
|
||||
color: $primary
|
||||
// Override for use of svg's from https://simpleicons.org/
|
||||
.icon svg
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
fill: currentColor
|
||||
.navbar-brand .navbar-item.logo
|
||||
padding: 0 1rem
|
||||
footer.footer
|
||||
padding: 3rem 0rem 0rem
|
||||
background-color: transparent
|
||||
|
||||
//Menu overrides
|
||||
$menu-item-color: $white-ter
|
||||
$menu-item-hover-color: $black !important
|
||||
$menu-item-hover-background-color: $black
|
||||
$menu-item-active-color: $kaldi-red-invert
|
||||
$menu-item-active-background-color: $kaldi-red-invert
|
||||
$menu-list-border-left: 1px solid $kaldi-red-invert
|
||||
$menu-label-color: $white-ter
|
||||
|
||||
|
||||
.menu-label
|
||||
font-size: 1em !important
|
||||
text-align: left
|
||||
.menu-list
|
||||
list-style: none !important
|
||||
text-align: left
|
||||
.social
|
||||
padding: 2em
|
||||
.social a
|
||||
padding: .5em .5em .3em .5em
|
||||
border-radius: 1em
|
||||
background-color: $white-ter
|
||||
margin: .5em
|
||||
width: 1em
|
||||
height: 1em
|
||||
vertical-align: middle
|
||||
display: inline
|
||||
|
||||
// blog roll
|
||||
.blog-list-item.is-featured
|
||||
background-color: #d6400033;
|
||||
.blog-list-item header
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
.blog-list-item .featured-thumbnail
|
||||
flex-basis: 35%;
|
||||
margin: 0 1.5em 0 0;
|
||||
|
||||
|
||||
@import "~bulma"
|
||||
|
||||
|
||||
// responsiveness
|
||||
+tablet-only
|
||||
.blog-list-item .featured-thumbnail
|
||||
flex-basis: 50%;
|
||||
|
||||
+mobile
|
||||
.blog-list-item header
|
||||
display: block
|
||||
.blog-list-item .featured-thumbnail
|
||||
text-align: center;
|
||||
max-width: 70%;
|
||||
margin: 0 0 1em;
|
||||
5
src/img/github-icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="438.549px" height="438.549px" viewBox="0 0 438.549 438.549" style="enable-background:new 0 0 438.549 438.549;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365 c-39.781,0-76.472,9.804-110.063,29.408c-33.596,19.605-60.192,46.204-79.8,79.8C9.803,148.168,0,184.854,0,224.63 c0,47.78,13.94,90.745,41.827,128.906c27.884,38.164,63.906,64.572,108.063,79.227c5.14,0.954,8.945,0.283,11.419-1.996 c2.475-2.282,3.711-5.14,3.711-8.562c0-0.571-0.049-5.708-0.144-15.417c-0.098-9.709-0.144-18.179-0.144-25.406l-6.567,1.136 c-4.187,0.767-9.469,1.092-15.846,1c-6.374-0.089-12.991-0.757-19.842-1.999c-6.854-1.231-13.229-4.086-19.13-8.559 c-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559 c-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-0.951-2.568-2.098-3.711-3.429c-1.142-1.331-1.997-2.663-2.568-3.997 c-0.572-1.335-0.098-2.43,1.427-3.289c1.525-0.859,4.281-1.276,8.28-1.276l5.708,0.853c3.807,0.763,8.516,3.042,14.133,6.851 c5.614,3.806,10.229,8.754,13.846,14.842c4.38,7.806,9.657,13.754,15.846,17.847c6.184,4.093,12.419,6.136,18.699,6.136 c6.28,0,11.704-0.476,16.274-1.423c4.565-0.952,8.848-2.383,12.847-4.285c1.713-12.758,6.377-22.559,13.988-29.41 c-10.848-1.14-20.601-2.857-29.264-5.14c-8.658-2.286-17.605-5.996-26.835-11.14c-9.235-5.137-16.896-11.516-22.985-19.126 c-6.09-7.614-11.088-17.61-14.987-29.979c-3.901-12.374-5.852-26.648-5.852-42.826c0-23.035,7.52-42.637,22.557-58.817 c-7.044-17.318-6.379-36.732,1.997-58.24c5.52-1.715,13.706-0.428,24.554,3.853c10.85,4.283,18.794,7.952,23.84,10.994 c5.046,3.041,9.089,5.618,12.135,7.708c17.705-4.947,35.976-7.421,54.818-7.421s37.117,2.474,54.823,7.421l10.849-6.849 c7.419-4.57,16.18-8.758,26.262-12.565c10.088-3.805,17.802-4.853,23.134-3.138c8.562,21.509,9.325,40.922,2.279,58.24 c15.036,16.18,22.559,35.787,22.559,58.817c0,16.178-1.958,30.497-5.853,42.966c-3.9,12.471-8.941,22.457-15.125,29.979 c-6.191,7.521-13.901,13.85-23.131,18.986c-9.232,5.14-18.182,8.85-26.84,11.136c-8.662,2.286-18.415,4.004-29.263,5.146 c9.894,8.562,14.842,22.077,14.842,40.539v60.237c0,3.422,1.19,6.279,3.572,8.562c2.379,2.279,6.136,2.95,11.276,1.995 c44.163-14.653,80.185-41.062,108.068-79.226c27.88-38.161,41.825-81.126,41.825-128.906 C438.536,184.851,428.728,148.168,409.132,114.573z"/>
|
||||
</g>
|
||||
<div xmlns="" id="divScriptsUsed" style="display: none"/><script xmlns="" id="globalVarsDetection" src="chrome-extension://cmkdbmfndkfgebldhnkbfhlneefdaaip/js/wrs_env.js"/></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
src/img/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 109 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns"><title>Logo</title><g transform="translate(-1470)" figma:type="canvas"><g style="mix-blend-mode:normal" figma:type="vector" transform="translate(1470)" fill="#f40"><use xlink:href="#b" style="mix-blend-mode:normal"/><use xlink:href="#c" style="mix-blend-mode:normal"/><use xlink:href="#d" style="mix-blend-mode:normal"/><use xlink:href="#e" style="mix-blend-mode:normal"/><use xlink:href="#f" style="mix-blend-mode:normal"/></g></g><defs><path id="b" d="M22.735 23.171c.283.323.053.829-.376.829h-5.907c-.285 0-.556-.121-.745-.333l-9.414-10.526v10.36c0 .276-.224.5-.5.5h-5.293c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v9.815l9.141-9.99c.19-.207.457-.325.738-.325h5.762c.437 0 .664.521.366.841l-9.851 10.563 10.287 11.767z"/><path id="c" d="M45.991 24c-.199 0-.38-.118-.459-.301l-2.024-4.669h-10.67l-2.024 4.669c-.079.183-.259.301-.459.301h-5.212c-.366 0-.608-.381-.453-.712l10.782-23c.082-.176.259-.288.453-.288h4.358c.194 0 .37.112.453.287l10.815 23c.156.332-.086.713-.452.713h-5.108zm-11.135-9.668h6.635l-3.317-7.694-3.317 7.694z"/><path id="d" d="M55.525 24c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v18.428h9.759c.276 0 .5.224.5.5v4.072c0 .276-.224.5-.5.5h-15.552z"/><path id="e" d="M75.279.5c0-.276.224-.5.5-.5h9.315c2.667 0 4.959.477 6.874 1.43 1.938.953 3.42 2.338 4.446 4.153 1.026 1.793 1.539 3.926 1.539 6.4 0 2.496-.513 4.652-1.539 6.468-1.003 1.793-2.474 3.166-4.412 4.119-1.915.953-4.218 1.43-6.908 1.43h-9.315c-.276 0-.5-.224-.5-.5v-23zm9.37 18.462c2.371 0 4.138-.579 5.301-1.736 1.163-1.157 1.744-2.905 1.744-5.242 0-2.338-.581-4.074-1.744-5.209-1.163-1.157-2.93-1.736-5.301-1.736h-3.078v13.923h3.078z"/><path id="f" d="M102.913 24c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v23c0 .276-.224.5-.5.5h-5.293z"/></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/img/social/facebook.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Facebook icon</title><path d="M22.676 0H1.324C.593 0 0 .593 0 1.324v21.352C0 23.408.593 24 1.324 24h11.494v-9.294H9.689v-3.621h3.129V8.41c0-3.099 1.894-4.785 4.659-4.785 1.325 0 2.464.097 2.796.141v3.24h-1.921c-1.5 0-1.792.721-1.792 1.771v2.311h3.584l-.465 3.63H16.56V24h6.115c.733 0 1.325-.592 1.325-1.324V1.324C24 .593 23.408 0 22.676 0"/></svg>
|
||||
|
After Width: | Height: | Size: 425 B |
1
src/img/social/instagram.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Instagram icon</title><path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
src/img/social/twitter.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter icon</title><path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z"/></svg>
|
||||
|
After Width: | Height: | Size: 757 B |
1
src/img/social/vimeo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Vimeo icon</title><path d="M23.977 6.416c-.105 2.338-1.739 5.543-4.894 9.609-3.268 4.247-6.026 6.37-8.29 6.37-1.409 0-2.578-1.294-3.553-3.881L5.322 11.4C4.603 8.816 3.834 7.522 3.01 7.522c-.179 0-.806.378-1.881 1.132L0 7.197c1.185-1.044 2.351-2.084 3.501-3.128C5.08 2.701 6.266 1.984 7.055 1.91c1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.507.539 2.45 1.131 3.674 1.776 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.868 3.434-5.757 6.762-5.637 2.473.06 3.628 1.664 3.493 4.797l-.013.01z"/></svg>
|
||||
|
After Width: | Height: | Size: 679 B |
13
src/pages/404.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout>
|
||||
<div>
|
||||
<h1>NOT FOUND</h1>
|
||||
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFoundPage
|
||||
19
src/pages/about/index.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
templateKey: 'about-page'
|
||||
path: /about
|
||||
title: About our values
|
||||
---
|
||||
### Shade-grown coffee
|
||||
Coffee is a small tree or shrub that grows in the forest understory in its wild form, and traditionally was grown commercially under other trees that provided shade. The forest-like structure of shade coffee farms provides habitat for a great number of migratory and resident species.
|
||||
|
||||
### Single origin
|
||||
Single-origin coffee is coffee grown within a single known geographic origin. Sometimes, this is a single farm or a specific collection of beans from a single country. The name of the coffee is then usually the place it was grown to whatever degree available.
|
||||
|
||||
### Sustainable farming
|
||||
Sustainable agriculture is farming in sustainable ways based on an understanding of ecosystem services, the study of relationships between organisms and their environment. What grows where and how it is grown are a matter of choice and careful consideration for nature and communities.
|
||||
|
||||
### Direct sourcing
|
||||
Direct trade is a form of sourcing practiced by some coffee roasters. Advocates of direct trade practices promote direct communication and price negotiation between buyer and farmer, along with systems that encourage and incentivize quality.
|
||||
|
||||
### Reinvest profits
|
||||
We want to truly empower the communities that bring amazing coffee to you. That’s why we reinvest 20% of our profits into farms, local businesses and schools everywhere our coffee is grown. You can see the communities grow and learn more about coffee farming on our blog.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
templateKey: blog-post
|
||||
title: Making sense of the SCAA’s new Flavor Wheel
|
||||
date: 2016-12-17T15:04:10.000Z
|
||||
featuredpost: false
|
||||
featuredimage: /img/flavor_wheel.jpg
|
||||
description: The Coffee Taster’s Flavor Wheel, the official resource used by coffee tasters, has been revised for the first time this year.
|
||||
tags:
|
||||
- flavor
|
||||
- tasting
|
||||
---
|
||||

|
||||
|
||||
The SCAA updated the wheel to reflect the finer nuances needed to describe flavors more precisely. The new descriptions are more detailed and hence allow cuppers to distinguish between more flavors.
|
||||
|
||||
While this is going to be a big change for professional coffee tasters, it means a lot to you as a consumer as well. We’ll explain how the wheel came to be, how pros use it and what the flavors actually mean.
|
||||
|
||||
## What the updates mean to you
|
||||
|
||||
The Specialty Coffee Association of America (SCAA), founded in 1982, is a non-profit trade organization for the specialty coffee industry. With members located in more than 40 countries, SCAA represents every segment of the specialty coffee industry, including:
|
||||
|
||||
* producers
|
||||
* roasters
|
||||
* importers/exporters
|
||||
* retailers
|
||||
* manufacturers
|
||||
* baristas
|
||||
|
||||
For over 30 years, SCAA has been dedicated to creating a vibrant specialty coffee community by recognizing, developing and promoting specialty coffee. SCAA sets and maintains quality standards for the industry, conducts market research, and provides education, training, resources, and business services for its members.
|
||||
|
||||
Coffee cupping, or coffee tasting, is the practice of observing the tastes and aromas of brewed coffee. It is a professional practice but can be done informally by anyone or by professionals known as "Q Graders". A standard coffee cupping procedure involves deeply sniffing the coffee, then loudly slurping the coffee so it spreads to the back of the tongue.
|
||||
|
||||
The coffee taster attempts to measure aspects of the coffee's taste, specifically the body (the texture or mouthfeel, such as oiliness), sweetness, acidity (a sharp and tangy feeling, like when biting into an orange), flavour (the characters in the cup), and aftertaste. Since coffee beans embody telltale flavours from the region where they were grown, cuppers may attempt to identify the coffee's origin.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
templateKey: blog-post
|
||||
title: A beginners’ guide to brewing with Chemex
|
||||
date: 2017-01-04T15:04:10.000Z
|
||||
featuredpost: false
|
||||
featuredimage: /img/chemex.jpg
|
||||
description: Brewing with a Chemex probably seems like a complicated, time-consuming ordeal, but once you get used to the process, it becomes a soothing ritual that's worth the effort every time.
|
||||
tags:
|
||||
- brewing
|
||||
- chemex
|
||||
---
|
||||

|
||||
|
||||
This week we’ll **take** a look at all the steps required to make astonishing coffee with a Chemex at home. The Chemex Coffeemaker is a manual, pour-over style glass-container coffeemaker that Peter Schlumbohm invented in 1941, and which continues to be manufactured by the Chemex Corporation in Chicopee, Massachusetts.
|
||||
|
||||
In 1958, designers at the [Illinois Institute of Technology](https://www.spacefarm.digital) said that the Chemex Coffeemaker is _"one of the best-designed products of modern times"_, and so is included in the collection of the Museum of Modern Art in New York City.
|
||||
|
||||
## The little secrets of Chemex brewing
|
||||
|
||||
The Chemex Coffeemaker consists of an hourglass-shaped glass flask with a conical funnel-like neck (rather than the cylindrical neck of an Erlenmeyer flask) and uses proprietary filters, made of bonded paper (thicker-gauge paper than the standard paper filters for a drip-method coffeemaker) that removes most of the coffee oils, brewing coffee with a taste that is different than coffee brewed in other coffee-making systems; also, the thicker paper of the Chemex coffee filters may assist in removing cafestol, a cholesterol-containing compound found in coffee oils. Here’s three important tips newbies forget about:
|
||||
|
||||
1. Always buy dedicated Chemex filters.
|
||||
2. Use a scale, don’t try to eyeball it.
|
||||
3. Never skip preheating the glass.
|
||||
4. Timing is key, don’t forget the clock.
|
||||
|
||||
The most visually distinctive feature of the Chemex is the heatproof wooden collar around the neck, allowing it to be handled and poured when full of hot water. This is turned, then split in two to allow it to fit around the glass neck. The two pieces are held loosely in place by a tied leather thong. The pieces are not tied tightly and can still move slightly, retained by the shape of the conical glass.
|
||||
|
||||
For a design piece that became popular post-war at a time of Modernism and precision manufacture, this juxtaposition of natural wood and the organic nature of a hand-tied knot with the laboratory nature of glassware was a distinctive feature of its appearance.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
templateKey: 'blog-post'
|
||||
title: 'Just in: small batch of Jamaican Blue Mountain in store next week'
|
||||
date: 2017-01-04T15:04:10.000Z
|
||||
featuredpost: true
|
||||
description: >-
|
||||
We’re proud to announce that we’ll be offering a small batch of Jamaica Blue
|
||||
Mountain coffee beans in our store next week.
|
||||
tags:
|
||||
- jamaica
|
||||
- green beans
|
||||
- flavor
|
||||
- tasting
|
||||
---
|
||||
|
||||
We expect the shipment of a limited quantity of green beans next Monday. We’ll be offering the roasted beans from Tuesday, but quantities are limited, so be quick.
|
||||
|
||||
Blue Mountain Peak is the highest mountain in Jamaica and one of the highest peaks in the Caribbean at 7,402 ft. It is the home of Blue Mountain coffee and their famous tours. It is located on the border of the Portland and Saint Thomas parishes of Jamaica.
|
||||
|
||||
## A little history
|
||||
|
||||
The Blue Mountains are considered by many to be a hiker's and camper's paradise. The traditional Blue Mountain trek is a 7-mile hike to the peak and consists of a 3,000-foot increase in elevation. Jamaicans prefer to reach the peak at sunrise, thus the 3–4 hour hike is usually undertaken in darkness. Since the sky is usually very clear in the mornings, Cuba can be seen in the distance.
|
||||
|
||||
>Some of the plants found on the Blue Mountain cannot be found anywhere else in the world and they are often of a dwarfed sort.
|
||||
|
||||
This is mainly due to the cold climate which inhibits growth. The small coffee farming communities of Claverty Cottage and Hagley Gap are located near the peak.
|
||||
|
||||
## What you need to know before trying
|
||||
|
||||
Jamaican Blue Mountain Coffee or Jamaica Blue Mountain Coffee is a classification of coffee grown in the Blue Mountains of Jamaica. The best lots of Blue Mountain coffee are noted for their mild flavor and lack of bitterness. Over the past few decades, this coffee has developed a reputation that has made it one of the most expensive and sought-after coffees in the world. Over 80% of all Jamaican Blue Mountain Coffee is exported to Japan. In addition to its use for brewed coffee, the beans are the flavor base of Tia Maria coffee liqueur.
|
||||
|
||||
Jamaican Blue Mountain Coffee is a globally protected certification mark, meaning only coffee certified by the Coffee Industry Board of Jamaica can be labeled as such. It comes from a recognized growing region in the Blue Mountain region of Jamaica, and its cultivation is monitored by the Coffee Industry Board of Jamaica.
|
||||
|
||||
The Blue Mountains are generally located between Kingston to the south and Port Antonio to the north. Rising 7,402 ft, they are some of the highest mountains in the Caribbean. The climate of the region is cool and misty with high rainfall. The soil is rich, with excellent drainage. This combination of climate and soil is considered ideal for coffee.
|
||||
38
src/pages/blog/index.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react'
|
||||
|
||||
import Layout from '../../components/Layout'
|
||||
import BlogRoll from '../../components/BlogRoll'
|
||||
|
||||
export default class BlogIndexPage extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<div
|
||||
className="full-width-image-container margin-top-0"
|
||||
style={{
|
||||
backgroundImage: `url('/img/blog-index.jpg')`,
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
className="has-text-weight-bold is-size-1"
|
||||
style={{
|
||||
boxShadow: '0.5rem 0 0 #f40, -0.5rem 0 0 #f40',
|
||||
backgroundColor: '#f40',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
Latest Stories
|
||||
</h1>
|
||||
</div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<BlogRoll />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
50
src/pages/contact/examples.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react'
|
||||
import Link from 'gatsby-link'
|
||||
import Layout from '../../components/Layout'
|
||||
|
||||
export default class Index extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<h1>Hi people</h1>
|
||||
<p>
|
||||
This is an example site integrating Netlify’s form handling with
|
||||
Gatsby
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/contact">Basic contact form</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/contact/file-upload/">Form with file upload</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
<h3>Forms stop working after upgrading to Gatsby v2</h3>
|
||||
<p>
|
||||
This can be caused by the offline-plugin.{' '}
|
||||
<a href="https://github.com/gatsbyjs/gatsby/issues/7997#issuecomment-419749232">
|
||||
Workaround
|
||||
</a>{' '}
|
||||
is to use <code>?no-cache=1</code> in the POST url to prevent
|
||||
the service worker from handling form submissions
|
||||
</p>
|
||||
<h3>Adding reCAPTCHA</h3>
|
||||
<p>
|
||||
If you are planning to add reCAPTCHA please go to{' '}
|
||||
<a href="https://github.com/imorente/gatsby-netlify-form-example">
|
||||
imorente/gatsby-netlify-form-example
|
||||
</a>{' '}
|
||||
for a working example.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
108
src/pages/contact/file-upload.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react'
|
||||
import { navigate } from 'gatsby-link'
|
||||
import Layout from '../../components/Layout'
|
||||
|
||||
function encode(data) {
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
formData.append(key, data[key])
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
export default class Contact extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value })
|
||||
}
|
||||
|
||||
handleAttachment = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.files[0] })
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
const form = e.target
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
body: encode({
|
||||
'form-name': form.getAttribute('name'),
|
||||
...this.state,
|
||||
}),
|
||||
})
|
||||
.then(() => navigate(form.getAttribute('action')))
|
||||
.catch((error) => alert(error))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<h1>File Upload</h1>
|
||||
<form
|
||||
name="file-upload"
|
||||
method="post"
|
||||
action="/contact/thanks/"
|
||||
data-netlify="true"
|
||||
data-netlify-honeypot="bot-field"
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
{/* The `form-name` hidden field is required to support form submissions without JavaScript */}
|
||||
<input type="hidden" name="form-name" value="file-upload" />
|
||||
<div hidden>
|
||||
<label>
|
||||
Don’t fill this out:{' '}
|
||||
<input name="bot-field" onChange={this.handleChange} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label" htmlFor={'name'}>
|
||||
Your name
|
||||
</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type={'text'}
|
||||
name={'name'}
|
||||
onChange={this.handleChange}
|
||||
id={'name'}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="file">
|
||||
<label className="file-label">
|
||||
<input
|
||||
className="file-input"
|
||||
type="file"
|
||||
name="attachment"
|
||||
onChange={this.handleAttachment}
|
||||
/>
|
||||
<span className="file-cta">
|
||||
<span className="file-label">Choose a file…</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<button className="button is-link" type="submit">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
115
src/pages/contact/index.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import React from 'react'
|
||||
import { navigate } from 'gatsby-link'
|
||||
import Layout from '../../components/Layout'
|
||||
|
||||
function encode(data) {
|
||||
return Object.keys(data)
|
||||
.map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
|
||||
.join('&')
|
||||
}
|
||||
|
||||
export default class Index extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { isValidated: false }
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value })
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
const form = e.target
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: encode({
|
||||
'form-name': form.getAttribute('name'),
|
||||
...this.state,
|
||||
}),
|
||||
})
|
||||
.then(() => navigate(form.getAttribute('action')))
|
||||
.catch((error) => alert(error))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<h1>Contact</h1>
|
||||
<form
|
||||
name="contact"
|
||||
method="post"
|
||||
action="/contact/thanks/"
|
||||
data-netlify="true"
|
||||
data-netlify-honeypot="bot-field"
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
{/* The `form-name` hidden field is required to support form submissions without JavaScript */}
|
||||
<input type="hidden" name="form-name" value="contact" />
|
||||
<div hidden>
|
||||
<label>
|
||||
Don’t fill this out:{' '}
|
||||
<input name="bot-field" onChange={this.handleChange} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label" htmlFor={'name'}>
|
||||
Your name
|
||||
</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type={'text'}
|
||||
name={'name'}
|
||||
onChange={this.handleChange}
|
||||
id={'name'}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label" htmlFor={'email'}>
|
||||
Email
|
||||
</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type={'email'}
|
||||
name={'email'}
|
||||
onChange={this.handleChange}
|
||||
id={'email'}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label" htmlFor={'message'}>
|
||||
Message
|
||||
</label>
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
name={'message'}
|
||||
onChange={this.handleChange}
|
||||
id={'message'}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<button className="button is-link" type="submit">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
15
src/pages/contact/thanks.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import Layout from '../../components/Layout'
|
||||
|
||||
export default () => (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<h1>Thank you!</h1>
|
||||
<p>This is a custom thank you page for form submissions</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
70
src/pages/index.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
templateKey: index-page
|
||||
title: Great coffee with a conscience
|
||||
image: /img/home-jumbotron.jpg
|
||||
heading: Great coffee with a conscience
|
||||
subheading: Support sustainable farming while enjoying a cup
|
||||
mainpitch:
|
||||
title: Why Kaldi
|
||||
description: >
|
||||
Kaldi is the coffee store for everyone who believes that great coffee
|
||||
shouldn't just taste good, it should do good too. We source all of our beans
|
||||
directly from small scale sustainable farmers and make sure part of the
|
||||
profits are reinvested in their communities.
|
||||
description: >-
|
||||
Kaldi is the ultimate spot for coffee lovers who want to learn about their
|
||||
java’s origin and support the farmers that grew it. We take coffee production,
|
||||
roasting and brewing seriously and we’re glad to pass that knowledge to
|
||||
anyone.
|
||||
intro:
|
||||
blurbs:
|
||||
- image: /img/coffee.png
|
||||
text: >
|
||||
We sell green and roasted coffee beans that are sourced directly from
|
||||
independent farmers and farm cooperatives. We’re proud to offer a
|
||||
variety of coffee beans grown with great care for the environment and
|
||||
local communities. Check our post or contact us directly for current
|
||||
availability.
|
||||
- image: /img/coffee-gear.png
|
||||
text: >
|
||||
We offer a small, but carefully curated selection of brewing gear and
|
||||
tools for every taste and experience level. No matter if you roast your
|
||||
own beans or just bought your first french press, you’ll find a gadget
|
||||
to fall in love with in our shop.
|
||||
- image: /img/tutorials.png
|
||||
text: >
|
||||
Love a great cup of coffee, but never knew how to make one? Bought a
|
||||
fancy new Chemex but have no clue how to use it? Don't worry, we’re here
|
||||
to help. You can schedule a custom 1-on-1 consultation with our baristas
|
||||
to learn anything you want to know about coffee roasting and brewing.
|
||||
Email us or call the store for details.
|
||||
- image: /img/meeting-space.png
|
||||
text: >
|
||||
We believe that good coffee has the power to bring people together.
|
||||
That’s why we decided to turn a corner of our shop into a cozy meeting
|
||||
space where you can hang out with fellow coffee lovers and learn about
|
||||
coffee making techniques. All of the artwork on display there is for
|
||||
sale. The full price you pay goes to the artist.
|
||||
heading: What we offer
|
||||
description: >
|
||||
Kaldi is the ultimate spot for coffee lovers who want to learn about their
|
||||
java’s origin and support the farmers that grew it. We take coffee
|
||||
production, roasting and brewing seriously and we’re glad to pass that
|
||||
knowledge to anyone. This is an edit via identity...
|
||||
main:
|
||||
heading: Great coffee with no compromises
|
||||
description: >
|
||||
We hold our coffee to the highest standards from the shrub to the cup.
|
||||
That’s why we’re meticulous and transparent about each step of the coffee’s
|
||||
journey. We personally visit each farm to make sure the conditions are
|
||||
optimal for the plants, farmers and the local environment.
|
||||
image1:
|
||||
alt: A close-up of a paper filter filled with ground coffee
|
||||
image: /img/products-grid3.jpg
|
||||
image2:
|
||||
alt: A green cup of a coffee on a wooden table
|
||||
image: /img/products-grid2.jpg
|
||||
image3:
|
||||
alt: Coffee beans
|
||||
image: /img/products-grid1.jpg
|
||||
---
|
||||
101
src/pages/products/index.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
templateKey: 'product-page'
|
||||
path: /products
|
||||
title: Our Coffee
|
||||
image: /img/jumbotron.jpg
|
||||
heading: Great coffee with a conscience
|
||||
description: >-
|
||||
Kaldi is the ultimate spot for coffee lovers who want to learn about their
|
||||
java’s origin and support the farmers that grew it. We take coffee production,
|
||||
roasting and brewing seriously and we’re glad to pass that knowledge to
|
||||
anyone.
|
||||
intro:
|
||||
blurbs:
|
||||
- image: /img/coffee.png
|
||||
text: >
|
||||
We sell green and roasted coffee beans that are sourced directly from
|
||||
independent farmers and farm cooperatives. We’re proud to offer a
|
||||
variety of coffee beans grown with great care for the environment and
|
||||
local communities. Check our post or contact us directly for current
|
||||
availability.
|
||||
- image: /img/coffee-gear.png
|
||||
text: >
|
||||
We offer a small, but carefully curated selection of brewing gear and
|
||||
tools for every taste and experience level. No matter if you roast your
|
||||
own beans or just bought your first french press, you’ll find a gadget
|
||||
to fall in love with in our shop.
|
||||
- image: /img/tutorials.png
|
||||
text: >
|
||||
Love a great cup of coffee, but never knew how to make one? Bought a
|
||||
fancy new Chemex but have no clue how to use it? Don't worry, we’re here
|
||||
to help. You can schedule a custom 1-on-1 consultation with our baristas
|
||||
to learn anything you want to know about coffee roasting and brewing.
|
||||
Email us or call the store for details.
|
||||
- image: /img/meeting-space.png
|
||||
text: >
|
||||
We believe that good coffee has the power to bring people together.
|
||||
That’s why we decided to turn a corner of our shop into a cozy meeting
|
||||
space where you can hang out with fellow coffee lovers and learn about
|
||||
coffee making techniques. All of the artwork on display there is for
|
||||
sale. The full price you pay goes to the artist.
|
||||
heading: What we offer
|
||||
description: >
|
||||
Kaldi is the ultimate spot for coffee lovers who want to learn about their
|
||||
java’s origin and support the farmers that grew it. We take coffee
|
||||
production, roasting and brewing seriously and we’re glad to pass that
|
||||
knowledge to anyone. This is an edit via identity...
|
||||
main:
|
||||
heading: Great coffee with no compromises
|
||||
description: >
|
||||
We hold our coffee to the highest standards from the shrub to the cup.
|
||||
That’s why we’re meticulous and transparent about each step of the coffee’s
|
||||
journey. We personally visit each farm to make sure the conditions are
|
||||
optimal for the plants, farmers and the local environment.
|
||||
image1:
|
||||
alt: A close-up of a paper filter filled with ground coffee
|
||||
image: /img/products-grid3.jpg
|
||||
image2:
|
||||
alt: A green cup of a coffee on a wooden table
|
||||
image: /img/products-grid2.jpg
|
||||
image3:
|
||||
alt: Coffee beans
|
||||
image: /img/products-grid1.jpg
|
||||
testimonials:
|
||||
- author: Elisabeth Kaurismäki
|
||||
quote: >-
|
||||
The first time I tried Kaldi’s coffee, I couldn’t even believe that was
|
||||
the same thing I’ve been drinking every morning.
|
||||
- author: Philipp Trommler
|
||||
quote: >-
|
||||
Kaldi is the place to go if you want the best quality coffee. I love their
|
||||
stance on empowering farmers and transparency.
|
||||
full_image: /img/products-full-width.jpg
|
||||
pricing:
|
||||
heading: Monthly subscriptions
|
||||
description: >-
|
||||
We make it easy to make great coffee a part of your life. Choose one of our
|
||||
monthly subscription plans to receive great coffee at your doorstep each
|
||||
month. Contact us about more details and payment info.
|
||||
plans:
|
||||
- description: Perfect for the drinker who likes to enjoy 1-2 cups per day.
|
||||
items:
|
||||
- 3 lbs of coffee per month
|
||||
- Green or roasted beans"
|
||||
- One or two varieties of beans"
|
||||
plan: Small
|
||||
price: '50'
|
||||
- description: 'Great for avid drinkers, java-loving couples and bigger crowds'
|
||||
items:
|
||||
- 6 lbs of coffee per month
|
||||
- Green or roasted beans
|
||||
- Up to 4 different varieties of beans
|
||||
plan: Big
|
||||
price: '80'
|
||||
- description: Want a few tiny batches from different varieties? Try our custom plan
|
||||
items:
|
||||
- Whatever you need
|
||||
- Green or roasted beans
|
||||
- Unlimited varieties
|
||||
plan: Custom
|
||||
price: '??'
|
||||
---
|
||||
57
src/pages/tags/index.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react'
|
||||
import { kebabCase } from 'lodash'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import Layout from '../../components/Layout'
|
||||
|
||||
const TagsPage = ({
|
||||
data: {
|
||||
allMarkdownRemark: { group },
|
||||
site: {
|
||||
siteMetadata: { title },
|
||||
},
|
||||
},
|
||||
}) => (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<Helmet title={`Tags | ${title}`} />
|
||||
<div className="container content">
|
||||
<div className="columns">
|
||||
<div
|
||||
className="column is-10 is-offset-1"
|
||||
style={{ marginBottom: '6rem' }}
|
||||
>
|
||||
<h1 className="title is-size-2 is-bold-light">Tags</h1>
|
||||
<ul className="taglist">
|
||||
{group.map((tag) => (
|
||||
<li key={tag.fieldValue}>
|
||||
<Link to={`/tags/${kebabCase(tag.fieldValue)}/`}>
|
||||
{tag.fieldValue} ({tag.totalCount})
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default TagsPage
|
||||
|
||||
export const tagPageQuery = graphql`
|
||||
query TagsQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
}
|
||||
}
|
||||
allMarkdownRemark(limit: 1000) {
|
||||
group(field: frontmatter___tags) {
|
||||
fieldValue
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
63
src/templates/about-page.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { graphql } from 'gatsby'
|
||||
import Layout from '../components/Layout'
|
||||
import Content, { HTMLContent } from '../components/Content'
|
||||
|
||||
export const AboutPageTemplate = ({ title, content, contentComponent }) => {
|
||||
const PageContent = contentComponent || Content
|
||||
|
||||
return (
|
||||
<section className="section section--gradient">
|
||||
<div className="container">
|
||||
<div className="columns">
|
||||
<div className="column is-10 is-offset-1">
|
||||
<div className="section">
|
||||
<h2 className="title is-size-3 has-text-weight-bold is-bold-light">
|
||||
{title}
|
||||
</h2>
|
||||
<PageContent className="content" content={content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
AboutPageTemplate.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string,
|
||||
contentComponent: PropTypes.func,
|
||||
}
|
||||
|
||||
const AboutPage = ({ data }) => {
|
||||
const { markdownRemark: post } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<AboutPageTemplate
|
||||
contentComponent={HTMLContent}
|
||||
title={post.frontmatter.title}
|
||||
content={post.html}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
AboutPage.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default AboutPage
|
||||
|
||||
export const aboutPageQuery = graphql`
|
||||
query AboutPage($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
html
|
||||
frontmatter {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
103
src/templates/blog-post.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { kebabCase } from 'lodash'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { graphql, Link } from 'gatsby'
|
||||
import Layout from '../components/Layout'
|
||||
import Content, { HTMLContent } from '../components/Content'
|
||||
|
||||
export const BlogPostTemplate = ({
|
||||
content,
|
||||
contentComponent,
|
||||
description,
|
||||
tags,
|
||||
title,
|
||||
helmet,
|
||||
}) => {
|
||||
const PostContent = contentComponent || Content
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
{helmet || ''}
|
||||
<div className="container content">
|
||||
<div className="columns">
|
||||
<div className="column is-10 is-offset-1">
|
||||
<h1 className="title is-size-2 has-text-weight-bold is-bold-light">
|
||||
{title}
|
||||
</h1>
|
||||
<p>{description}</p>
|
||||
<PostContent content={content} />
|
||||
{tags && tags.length ? (
|
||||
<div style={{ marginTop: `4rem` }}>
|
||||
<h4>Tags</h4>
|
||||
<ul className="taglist">
|
||||
{tags.map((tag) => (
|
||||
<li key={tag + `tag`}>
|
||||
<Link to={`/tags/${kebabCase(tag)}/`}>{tag}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
BlogPostTemplate.propTypes = {
|
||||
content: PropTypes.node.isRequired,
|
||||
contentComponent: PropTypes.func,
|
||||
description: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
helmet: PropTypes.object,
|
||||
}
|
||||
|
||||
const BlogPost = ({ data }) => {
|
||||
const { markdownRemark: post } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<BlogPostTemplate
|
||||
content={post.html}
|
||||
contentComponent={HTMLContent}
|
||||
description={post.frontmatter.description}
|
||||
helmet={
|
||||
<Helmet titleTemplate="%s | Blog">
|
||||
<title>{`${post.frontmatter.title}`}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={`${post.frontmatter.description}`}
|
||||
/>
|
||||
</Helmet>
|
||||
}
|
||||
tags={post.frontmatter.tags}
|
||||
title={post.frontmatter.title}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
BlogPost.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
markdownRemark: PropTypes.object,
|
||||
}),
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query BlogPostByID($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
id
|
||||
html
|
||||
frontmatter {
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
title
|
||||
description
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
193
src/templates/index-page.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
|
||||
import Layout from '../components/Layout'
|
||||
import Features from '../components/Features'
|
||||
import BlogRoll from '../components/BlogRoll'
|
||||
|
||||
export const IndexPageTemplate = ({
|
||||
image,
|
||||
title,
|
||||
heading,
|
||||
subheading,
|
||||
mainpitch,
|
||||
description,
|
||||
intro,
|
||||
}) => (
|
||||
<div>
|
||||
<div
|
||||
className="full-width-image margin-top-0"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
!!image.childImageSharp ? image.childImageSharp.fluid.src : image
|
||||
})`,
|
||||
backgroundPosition: `top left`,
|
||||
backgroundAttachment: `fixed`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: '150px',
|
||||
lineHeight: '1',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'left',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
className="has-text-weight-bold is-size-3-mobile is-size-2-tablet is-size-1-widescreen"
|
||||
style={{
|
||||
boxShadow:
|
||||
'rgb(255, 68, 0) 0.5rem 0px 0px, rgb(255, 68, 0) -0.5rem 0px 0px',
|
||||
backgroundColor: 'rgb(255, 68, 0)',
|
||||
color: 'white',
|
||||
lineHeight: '1',
|
||||
padding: '0.25em',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<h3
|
||||
className="has-text-weight-bold is-size-5-mobile is-size-5-tablet is-size-4-widescreen"
|
||||
style={{
|
||||
boxShadow:
|
||||
'rgb(255, 68, 0) 0.5rem 0px 0px, rgb(255, 68, 0) -0.5rem 0px 0px',
|
||||
backgroundColor: 'rgb(255, 68, 0)',
|
||||
color: 'white',
|
||||
lineHeight: '1',
|
||||
padding: '0.25em',
|
||||
}}
|
||||
>
|
||||
{subheading}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section section--gradient">
|
||||
<div className="container">
|
||||
<div className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-10 is-offset-1">
|
||||
<div className="content">
|
||||
<div className="content">
|
||||
<div className="tile">
|
||||
<h1 className="title">{mainpitch.title}</h1>
|
||||
</div>
|
||||
<div className="tile">
|
||||
<h3 className="subtitle">{mainpitch.description}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-12">
|
||||
<h3 className="has-text-weight-semibold is-size-2">
|
||||
{heading}
|
||||
</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Features gridItems={intro.blurbs} />
|
||||
<div className="columns">
|
||||
<div className="column is-12 has-text-centered">
|
||||
<Link className="btn" to="/products">
|
||||
See all products
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-12">
|
||||
<h3 className="has-text-weight-semibold is-size-2">
|
||||
Latest stories
|
||||
</h3>
|
||||
<BlogRoll />
|
||||
<div className="column is-12 has-text-centered">
|
||||
<Link className="btn" to="/blog">
|
||||
Read more
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
IndexPageTemplate.propTypes = {
|
||||
image: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
title: PropTypes.string,
|
||||
heading: PropTypes.string,
|
||||
subheading: PropTypes.string,
|
||||
mainpitch: PropTypes.object,
|
||||
description: PropTypes.string,
|
||||
intro: PropTypes.shape({
|
||||
blurbs: PropTypes.array,
|
||||
}),
|
||||
}
|
||||
|
||||
const IndexPage = ({ data }) => {
|
||||
const { frontmatter } = data.markdownRemark
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<IndexPageTemplate
|
||||
image={frontmatter.image}
|
||||
title={frontmatter.title}
|
||||
heading={frontmatter.heading}
|
||||
subheading={frontmatter.subheading}
|
||||
mainpitch={frontmatter.mainpitch}
|
||||
description={frontmatter.description}
|
||||
intro={frontmatter.intro}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
IndexPage.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
markdownRemark: PropTypes.shape({
|
||||
frontmatter: PropTypes.object,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default IndexPage
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query IndexPageTemplate {
|
||||
markdownRemark(frontmatter: { templateKey: { eq: "index-page" } }) {
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 2048, quality: 100) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
heading
|
||||
subheading
|
||||
mainpitch {
|
||||
title
|
||||
description
|
||||
}
|
||||
description
|
||||
intro {
|
||||
blurbs {
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 240, quality: 64) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
heading
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
247
src/templates/product-page.js
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { graphql } from 'gatsby'
|
||||
import Layout from '../components/Layout'
|
||||
import Features from '../components/Features'
|
||||
import Testimonials from '../components/Testimonials'
|
||||
import Pricing from '../components/Pricing'
|
||||
import PreviewCompatibleImage from '../components/PreviewCompatibleImage'
|
||||
|
||||
export const ProductPageTemplate = ({
|
||||
image,
|
||||
title,
|
||||
heading,
|
||||
description,
|
||||
intro,
|
||||
main,
|
||||
testimonials,
|
||||
fullImage,
|
||||
pricing,
|
||||
}) => (
|
||||
<div className="content">
|
||||
<div
|
||||
className="full-width-image-container margin-top-0"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
!!image.childImageSharp ? image.childImageSharp.fluid.src : image
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
className="has-text-weight-bold is-size-1"
|
||||
style={{
|
||||
boxShadow: '0.5rem 0 0 #f40, -0.5rem 0 0 #f40',
|
||||
backgroundColor: '#f40',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<section className="section section--gradient">
|
||||
<div className="container">
|
||||
<div className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-7 is-offset-1">
|
||||
<h3 className="has-text-weight-semibold is-size-2">{heading}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-10 is-offset-1">
|
||||
<Features gridItems={intro.blurbs} />
|
||||
<div className="columns">
|
||||
<div className="column is-7">
|
||||
<h3 className="has-text-weight-semibold is-size-3">
|
||||
{main.heading}
|
||||
</h3>
|
||||
<p>{main.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tile is-ancestor">
|
||||
<div className="tile is-vertical">
|
||||
<div className="tile">
|
||||
<div className="tile is-parent is-vertical">
|
||||
<article className="tile is-child">
|
||||
<PreviewCompatibleImage imageInfo={main.image1} />
|
||||
</article>
|
||||
</div>
|
||||
<div className="tile is-parent">
|
||||
<article className="tile is-child">
|
||||
<PreviewCompatibleImage imageInfo={main.image2} />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tile is-parent">
|
||||
<article className="tile is-child">
|
||||
<PreviewCompatibleImage imageInfo={main.image3} />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Testimonials testimonials={testimonials} />
|
||||
<div
|
||||
className="full-width-image-container"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
fullImage.childImageSharp
|
||||
? fullImage.childImageSharp.fluid.src
|
||||
: fullImage
|
||||
})`,
|
||||
}}
|
||||
/>
|
||||
<h2 className="has-text-weight-semibold is-size-2">
|
||||
{pricing.heading}
|
||||
</h2>
|
||||
<p className="is-size-5">{pricing.description}</p>
|
||||
<Pricing data={pricing.plans} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
ProductPageTemplate.propTypes = {
|
||||
image: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
title: PropTypes.string,
|
||||
heading: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
intro: PropTypes.shape({
|
||||
blurbs: PropTypes.array,
|
||||
}),
|
||||
main: PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
image1: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
image2: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
image3: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
}),
|
||||
testimonials: PropTypes.array,
|
||||
fullImage: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
pricing: PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
plans: PropTypes.array,
|
||||
}),
|
||||
}
|
||||
|
||||
const ProductPage = ({ data }) => {
|
||||
const { frontmatter } = data.markdownRemark
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProductPageTemplate
|
||||
image={frontmatter.image}
|
||||
title={frontmatter.title}
|
||||
heading={frontmatter.heading}
|
||||
description={frontmatter.description}
|
||||
intro={frontmatter.intro}
|
||||
main={frontmatter.main}
|
||||
testimonials={frontmatter.testimonials}
|
||||
fullImage={frontmatter.full_image}
|
||||
pricing={frontmatter.pricing}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
ProductPage.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
markdownRemark: PropTypes.shape({
|
||||
frontmatter: PropTypes.object,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default ProductPage
|
||||
|
||||
export const productPageQuery = graphql`
|
||||
query ProductPage($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 2048, quality: 100) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
heading
|
||||
description
|
||||
intro {
|
||||
blurbs {
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 240, quality: 64) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
heading
|
||||
description
|
||||
}
|
||||
main {
|
||||
heading
|
||||
description
|
||||
image1 {
|
||||
alt
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 526, quality: 92) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
image2 {
|
||||
alt
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 526, quality: 92) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
image3 {
|
||||
alt
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 1075, quality: 72) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
testimonials {
|
||||
author
|
||||
quote
|
||||
}
|
||||
full_image {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 2048, quality: 100) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
pricing {
|
||||
heading
|
||||
description
|
||||
plans {
|
||||
description
|
||||
items
|
||||
plan
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
74
src/templates/tags.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
class TagRoute extends React.Component {
|
||||
render() {
|
||||
const posts = this.props.data.allMarkdownRemark.edges
|
||||
const postLinks = posts.map((post) => (
|
||||
<li key={post.node.fields.slug}>
|
||||
<Link to={post.node.fields.slug}>
|
||||
<h2 className="is-size-2">{post.node.frontmatter.title}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
const tag = this.props.pageContext.tag
|
||||
const title = this.props.data.site.siteMetadata.title
|
||||
const totalCount = this.props.data.allMarkdownRemark.totalCount
|
||||
const tagHeader = `${totalCount} post${
|
||||
totalCount === 1 ? '' : 's'
|
||||
} tagged with “${tag}”`
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<section className="section">
|
||||
<Helmet title={`${tag} | ${title}`} />
|
||||
<div className="container content">
|
||||
<div className="columns">
|
||||
<div
|
||||
className="column is-10 is-offset-1"
|
||||
style={{ marginBottom: '6rem' }}
|
||||
>
|
||||
<h3 className="title is-size-4 is-bold-light">{tagHeader}</h3>
|
||||
<ul className="taglist">{postLinks}</ul>
|
||||
<p>
|
||||
<Link to="/tags/">Browse all tags</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TagRoute
|
||||
|
||||
export const tagPageQuery = graphql`
|
||||
query TagPage($tag: String) {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
}
|
||||
}
|
||||
allMarkdownRemark(
|
||||
limit: 1000
|
||||
sort: { fields: [frontmatter___date], order: DESC }
|
||||
filter: { frontmatter: { tags: { in: [$tag] } } }
|
||||
) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
66
static/admin/config.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
backend:
|
||||
name: git-gateway
|
||||
branch: master
|
||||
commit_messages:
|
||||
create: 'Create {{collection}} “{{slug}}”'
|
||||
update: 'Update {{collection}} “{{slug}}”'
|
||||
delete: 'Delete {{collection}} “{{slug}}”'
|
||||
uploadMedia: '[skip ci] Upload “{{path}}”'
|
||||
deleteMedia: '[skip ci] Delete “{{path}}”'
|
||||
|
||||
media_folder: static/img
|
||||
public_folder: /img
|
||||
|
||||
collections:
|
||||
- name: "blog"
|
||||
label: "Blog"
|
||||
folder: "src/pages/blog"
|
||||
create: true
|
||||
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
|
||||
fields:
|
||||
- {label: "Template Key", name: "templateKey", widget: "hidden", default: "blog-post"}
|
||||
- {label: "Title", name: "title", widget: "string"}
|
||||
- {label: "Publish Date", name: "date", widget: "datetime"}
|
||||
- {label: "Description", name: "description", widget: "text"}
|
||||
- {label: "Featured Post", name: "featuredpost", widget: "boolean"}
|
||||
- {label: "Featured Image", name: "featuredimage", widget: image}
|
||||
- {label: "Body", name: "body", widget: "markdown"}
|
||||
- {label: "Tags", name: "tags", widget: "list"}
|
||||
|
||||
- name: "pages"
|
||||
label: "Pages"
|
||||
files:
|
||||
- file: "src/pages/index.md"
|
||||
label: "Landing Page"
|
||||
name: "index"
|
||||
fields:
|
||||
- {label: "Template Key", name: "templateKey", widget: "hidden", default: "index-page"}
|
||||
- {label: Title, name: title, widget: string}
|
||||
- {label: Image, name: image, widget: image}
|
||||
- {label: Heading, name: heading, widget: string}
|
||||
- {label: Subheading, name: subheading, widget: string}
|
||||
- {label: Mainpitch, name: mainpitch, widget: object, fields: [{label: Title, name: title, widget: string}, {label: Description, name: description, widget: text}]}
|
||||
- {label: Description, name: description, widget: string}
|
||||
- {label: Intro, name: intro, widget: object, fields: [{label: Heading, name: heading, widget: string}, {label: Description, name: description, widget: text}, {label: Blurbs, name: blurbs, widget: list, fields: [{label: Image, name: image, widget: image}, {label: Text, name: text, widget: text}]}]}
|
||||
- {label: Main, name: main, widget: object, fields: [{label: Heading, name: heading, widget: string}, {label: Description, name: description, widget: text}, {label: Image1, name: image1, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}, {label: Image2, name: image2, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}, {label: Image3, name: image3, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}]}
|
||||
- file: "src/pages/about/index.md"
|
||||
label: "About"
|
||||
name: "about"
|
||||
fields:
|
||||
- {label: "Template Key", name: "templateKey", widget: "hidden", default: "about-page"}
|
||||
- {label: "Title", name: "title", widget: "string"}
|
||||
- {label: "Body", name: "body", widget: "markdown"}
|
||||
- file: "src/pages/products/index.md"
|
||||
label: "Products Page"
|
||||
name: "products"
|
||||
fields:
|
||||
- {label: "Template Key", name: "templateKey", widget: "hidden", default: "product-page"}
|
||||
- {label: Title, name: title, widget: string}
|
||||
- {label: Image, name: image, widget: image}
|
||||
- {label: Heading, name: heading, widget: string}
|
||||
- {label: Description, name: description, widget: string}
|
||||
- {label: Intro, name: intro, widget: object, fields: [{label: Heading, name: heading, widget: string}, {label: Description, name: description, widget: text}, {label: Blurbs, name: blurbs, widget: list, fields: [{label: Image, name: image, widget: image}, {label: Text, name: text, widget: text}]}]}
|
||||
- {label: Main, name: main, widget: object, fields: [{label: Heading, name: heading, widget: string}, {label: Description, name: description, widget: text}, {label: Image1, name: image1, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}, {label: Image2, name: image2, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}, {label: Image3, name: image3, widget: object, fields: [{label: Image, name: image, widget: image}, {label: Alt, name: alt, widget: string}]}]}
|
||||
- {label: Testimonials, name: testimonials, widget: list, fields: [{label: Quote, name: quote, widget: string}, {label: Author, name: author, widget: string}]}
|
||||
- {label: Full_image, name: full_image, widget: image}
|
||||
- {label: Pricing, name: pricing, widget: object, fields: [{label: Heading, name: heading, widget: string}, {label: Description, name: description, widget: string}, {label: Plans, name: plans, widget: list, fields: [{label: Plan, name: plan, widget: string}, {label: Price, name: price, widget: string}, {label: Description, name: description, widget: string}, {label: Items, name: items, widget: list}]}]}
|
||||
BIN
static/img/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/img/blog-index.jpg
Executable file
|
After Width: | Height: | Size: 161 KiB |
BIN
static/img/chemex.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
static/img/coffee-gear.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
static/img/coffee.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
static/img/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
static/img/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 925 B |
BIN
static/img/flavor_wheel.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
static/img/home-jumbotron.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
static/img/jumbotron.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
1
static/img/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 109 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns"><title>Logo</title><g transform="translate(-1470)" figma:type="canvas"><g style="mix-blend-mode:normal" figma:type="vector" transform="translate(1470)" fill="#f40"><use xlink:href="#b" style="mix-blend-mode:normal"/><use xlink:href="#c" style="mix-blend-mode:normal"/><use xlink:href="#d" style="mix-blend-mode:normal"/><use xlink:href="#e" style="mix-blend-mode:normal"/><use xlink:href="#f" style="mix-blend-mode:normal"/></g></g><defs><path id="b" d="M22.735 23.171c.283.323.053.829-.376.829h-5.907c-.285 0-.556-.121-.745-.333l-9.414-10.526v10.36c0 .276-.224.5-.5.5h-5.293c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v9.815l9.141-9.99c.19-.207.457-.325.738-.325h5.762c.437 0 .664.521.366.841l-9.851 10.563 10.287 11.767z"/><path id="c" d="M45.991 24c-.199 0-.38-.118-.459-.301l-2.024-4.669h-10.67l-2.024 4.669c-.079.183-.259.301-.459.301h-5.212c-.366 0-.608-.381-.453-.712l10.782-23c.082-.176.259-.288.453-.288h4.358c.194 0 .37.112.453.287l10.815 23c.156.332-.086.713-.452.713h-5.108zm-11.135-9.668h6.635l-3.317-7.694-3.317 7.694z"/><path id="d" d="M55.525 24c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v18.428h9.759c.276 0 .5.224.5.5v4.072c0 .276-.224.5-.5.5h-15.552z"/><path id="e" d="M75.279.5c0-.276.224-.5.5-.5h9.315c2.667 0 4.959.477 6.874 1.43 1.938.953 3.42 2.338 4.446 4.153 1.026 1.793 1.539 3.926 1.539 6.4 0 2.496-.513 4.652-1.539 6.468-1.003 1.793-2.474 3.166-4.412 4.119-1.915.953-4.218 1.43-6.908 1.43h-9.315c-.276 0-.5-.224-.5-.5v-23zm9.37 18.462c2.371 0 4.138-.579 5.301-1.736 1.163-1.157 1.744-2.905 1.744-5.242 0-2.338-.581-4.074-1.744-5.209-1.163-1.157-2.93-1.736-5.301-1.736h-3.078v13.923h3.078z"/><path id="f" d="M102.913 24c-.276 0-.5-.224-.5-.5v-23c0-.276.224-.5.5-.5h5.293c.276 0 .5.224.5.5v23c0 .276-.224.5-.5.5h-5.293z"/></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/img/meeting-space.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
static/img/og-image.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
static/img/products-full-width.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
static/img/products-grid1.jpg
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
static/img/products-grid2.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
static/img/products-grid3.jpg
Normal file
|
After Width: | Height: | Size: 146 KiB |
28
static/img/safari-pinned-tab.svg
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M387 5763 c-1 -1 -24 -3 -52 -4 -155 -9 -269 -96 -319 -244 -15 -46
|
||||
-16 -145 -13 -1225 2 -646 6 -1188 10 -1205 4 -16 12 -59 18 -95 24 -136 69
|
||||
-300 112 -405 180 -439 480 -792 869 -1024 275 -164 565 -258 888 -288 97 -9
|
||||
1416 -9 1535 0 119 9 189 19 272 38 27 6 55 12 63 14 164 34 384 123 565 228
|
||||
177 103 448 336 555 477 19 25 37 47 40 50 17 15 104 148 151 230 97 169 175
|
||||
359 209 510 7 30 16 65 21 77 l8 22 59 -23 c84 -33 149 -48 272 -63 119 -15
|
||||
341 -3 420 22 10 3 28 8 41 10 82 16 294 118 387 188 79 58 181 155 228 213
|
||||
59 75 151 222 159 254 1 3 11 26 23 52 63 140 92 307 87 498 -2 69 -6 136 -9
|
||||
150 -3 14 -9 41 -12 60 -20 119 -96 297 -182 425 -143 212 -311 346 -572 459
|
||||
-44 19 -171 53 -225 60 -22 3 -60 8 -85 12 -140 19 -350 -5 -497 -56 l-53 -18
|
||||
-1 26 c0 15 -1 90 -3 167 -2 126 -5 146 -27 195 -33 71 -71 115 -134 155 -54
|
||||
34 -77 42 -141 51 -35 5 -4663 12 -4667 7z m5552 -1236 c80 -23 186 -92 241
|
||||
-155 210 -242 147 -625 -130 -786 -198 -115 -453 -86 -614 71 -83 80 -79 64
|
||||
-78 341 0 136 1 267 1 291 1 40 6 49 66 107 73 71 119 99 215 130 86 28 209
|
||||
28 299 1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/img/tutorials.png
Normal file
|
After Width: | Height: | Size: 3 KiB |