├── .env.sample
├── cypress.json
├── src
├── assets
│ ├── images
│ │ ├── favicon.ico
│ │ ├── tabs-icon-01.svg
│ │ ├── tabs-icon-03.svg
│ │ ├── tabs-icon-02.svg
│ │ ├── logo.svg
│ │ ├── loader.svg
│ │ ├── feature-icon-01.svg
│ │ ├── feature-icon-03.svg
│ │ ├── feature-icon-02.svg
│ │ ├── feature-icon-04.svg
│ │ ├── hubspot-logo.svg
│ │ ├── facebook-logo.svg
│ │ ├── airbnb-logo.svg
│ │ ├── microsoft-logo.svg
│ │ ├── header-bg-left.svg
│ │ ├── tinder-logo.svg
│ │ ├── header-bg.svg
│ │ └── header-bg-right.svg
│ └── js
│ │ └── main.js
└── components
│ ├── header.js
│ ├── contact.js
│ ├── clients.js
│ ├── hero.js
│ ├── faq.js
│ ├── testimonials.js
│ ├── features.js
│ ├── blogs.js
│ ├── pricing.js
│ └── footer.js
├── cypress
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
├── support
│ ├── index.js
│ └── commands.js
└── integration
│ └── index-page.js
├── .gitignore
├── now.json
├── .babelrc
├── next.config.js
├── pages
├── _app.js
├── _document.js
├── blogs.js
└── index.js
├── LICENSE
├── package.json
└── README.md
/.env.sample:
--------------------------------------------------------------------------------
1 | BUTTER_CMS_API_KEY=something
2 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | { "baseUrl": "http://localhost:3000" }
2 |
--------------------------------------------------------------------------------
/src/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghoshnirmalya/testing-next.js-apps/HEAD/src/assets/images/favicon.ico
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | yarn-debug.log*
3 | yarn-error.log*
4 |
5 | # Dependency directories
6 | node_modules/
7 |
8 | # Yarn Integrity file
9 | .yarn-integrity
10 |
11 | # dotenv environment variables file
12 | .env
13 |
14 | # next.js build output
15 | .next
16 | out
17 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [{
4 | "src": "package.json",
5 | "use": "@now/static-build",
6 | "config": { "distDir": "out" }
7 | }],
8 | "build": {
9 | "env": {
10 | "BUTTER_CMS_API_KEY": "@butter-cms-api-key"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "preset-env": {},
7 | "transform-runtime": {},
8 | "styled-jsx": {},
9 | "class-properties": {}
10 | }
11 | ]
12 | ],
13 | "plugins": ["@babel/plugin-syntax-dynamic-import"]
14 | }
15 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withCSS = require("@zeit/next-css");
2 | const withImages = require("next-images");
3 | const webpack = require("webpack");
4 |
5 | require("dotenv").config();
6 |
7 | module.exports = withImages(
8 | withCSS({
9 | webpack: config => {
10 | config.plugins.push(new webpack.EnvironmentPlugin(process.env));
11 | return config;
12 | }
13 | })
14 | );
15 |
--------------------------------------------------------------------------------
/src/assets/images/tabs-icon-01.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/tabs-icon-03.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/tabs-icon-02.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | };
18 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import App, { Container } from "next/app";
3 | import dynamic from "next/dynamic";
4 | import withNProgress from "next-nprogress";
5 |
6 | import "../src/assets/css/style.css";
7 |
8 | class MyApp extends App {
9 | componentDidMount() {
10 | import("../src/assets/js/main.js");
11 | }
12 |
13 | render() {
14 | const { Component, pageProps } = this.props;
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default withNProgress()(MyApp);
29 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import "./commands";
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript } from "next/document";
2 |
3 | import Favicon from "../src/assets/images/favicon.ico";
4 |
5 | export default class MyDocument extends Document {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Link from "next/link";
4 |
5 | function Header({ data }) {
6 | return (
7 |
22 | );
23 | }
24 |
25 | Header.propTypes = {
26 | data: PropTypes.object.isRequired
27 | };
28 |
29 | export default Header;
30 |
--------------------------------------------------------------------------------
/src/components/contact.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function Contact({ data }) {
5 | return (
6 |
7 |
8 |
9 |
{data.fields.description}
10 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | Contact.propTypes = {
25 | data: PropTypes.object.isRequired
26 | };
27 |
28 | export default Contact;
29 |
--------------------------------------------------------------------------------
/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 | Blue
--------------------------------------------------------------------------------
/src/components/clients.js:
--------------------------------------------------------------------------------
1 | import React, { Component, useState, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function Clients({ data }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | {data.fields.clients.map((client, index) => {
12 | return (
13 |
14 |
15 |
16 | );
17 | })}
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | Clients.propTypes = {
27 | data: PropTypes.object.isRequired
28 | };
29 |
30 | export default Clients;
31 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/src/components/hero.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function Hero({ data }) {
5 | return (
6 |
27 | );
28 | }
29 |
30 | Hero.propTypes = {
31 | data: PropTypes.object.isRequired
32 | };
33 |
34 | export default Hero;
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 ButterCMS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/assets/images/loader.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/faq.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function FAQ({ data }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
{data.fields.title}
11 |
12 | {data.fields.faqs.map((faq, index) => {
13 | return (
14 |
15 |
16 |
{faq.question}
17 |
18 |
19 |
22 |
23 | );
24 | })}
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | FAQ.propTypes = {
34 | data: PropTypes.object.isRequired
35 | };
36 |
37 | export default FAQ;
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "buttercms-marketing-site-nextjs-react",
3 | "version": "0.0.1",
4 | "description": "Designing a marketing site using ButterCMS and Next.js",
5 | "author": "Nirmalya Ghosh",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "next",
9 | "build": "next build",
10 | "export": "yarn build && next export",
11 | "cypress:open": "cypress open",
12 | "now-build": "yarn export"
13 | },
14 | "dependencies": {
15 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
16 | "@zeit/next-css": "^1.0.1",
17 | "buttercms": "^1.1.4",
18 | "cypress": "^3.2.0",
19 | "dotenv": "^6.2.0",
20 | "next": "^14.1.1",
21 | "next-images": "^1.0.4",
22 | "next-nprogress": "^1.4.0",
23 | "prop-types": "^15.6.2",
24 | "react": "^16.8.1",
25 | "react-dom": "^16.8.1",
26 | "styled-jsx": "3.0.0"
27 | },
28 | "husky": {
29 | "hooks": {
30 | "pre-commit": "lint-staged"
31 | }
32 | },
33 | "lint-staged": {
34 | "*.{js,json,css,md}": [
35 | "prettier --write",
36 | "git add"
37 | ]
38 | },
39 | "devDependencies": {
40 | "husky": "^1.3.1",
41 | "lint-staged": "^8.1.3",
42 | "prettier": "^1.18.2"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/testimonials.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function Testimonials({ data }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
{data.fields.title}
11 |
12 |
13 | {data.fields.testimonials.map((testimonial, index) => {
14 | return (
15 |
16 |
17 |
18 |
19 |
{testimonial.body}
20 |
21 |
22 |
27 |
28 |
29 | );
30 | })}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | Testimonials.propTypes = {
39 | data: PropTypes.object.isRequired
40 | };
41 |
42 | export default Testimonials;
43 |
--------------------------------------------------------------------------------
/src/components/features.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import FeatureIcon1 from "../assets/images/feature-icon-01.svg";
5 |
6 | function Features({ data }) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
{data.fields.title}
14 |
{data.fields.sub_title}
15 |
16 |
17 |
18 | {data.fields.features.map((feature, index) => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
{feature.title}
26 |
{feature.sub_title}
27 |
28 |
29 | );
30 | })}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | Features.propTypes = {
39 | data: PropTypes.object.isRequired
40 | };
41 |
42 | export default Features;
43 |
--------------------------------------------------------------------------------
/src/components/blogs.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function createMarkup(html) {
5 | return { __html: html };
6 | }
7 |
8 | function Blogs({ data }) {
9 | return (
10 |
11 |
12 |
13 |
14 |
Blogs
15 |
16 |
17 | {data.map((blog, index) => {
18 | return (
19 |
37 | );
38 | })}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | Blogs.propTypes = {
47 | data: PropTypes.array.isRequired
48 | };
49 |
50 | export default Blogs;
51 |
--------------------------------------------------------------------------------
/src/assets/images/feature-icon-01.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/feature-icon-03.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/feature-icon-02.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Testing Next.js apps
2 |
3 | This app demonstrates how you can test a Next.js app using Cypress.
4 |
5 |
6 |
7 | ## Demo
8 |
9 | Please check out the ZEIT demo at https://testing-nextjs-apps.ghoshnirmalya.now.sh/.
10 |
11 | ## Development
12 |
13 | ```sh
14 | $ git clone https://github.com/ghoshnirmalya/testing-next.js-apps
15 | $ cd testing-next.js-apps
16 | $ yarn install
17 | ```
18 |
19 | If you prefer `npm`, you can do `npm install` instead of `yarn install`.
20 |
21 | #### Running the app
22 |
23 | ```sh
24 | $ yarn dev
25 | ```
26 |
27 | If you prefer `npm`, you can do `npm run dev` instead of `yarn dev`.
28 |
29 | #### Building the app
30 |
31 | ```sh
32 | $ yarn build
33 | ```
34 |
35 | If you prefer `npm`, you can do `npm run build` instead of `yarn build`.
36 |
37 | #### Exporting the app
38 |
39 | ```sh
40 | $ yarn export
41 | ```
42 |
43 | If you prefer `npm`, you can do `npm run export` instead of `yarn export`.
44 |
45 | #### Running Cypress
46 |
47 | ```sh
48 | $ yarn run cypress open
49 | ```
50 |
51 | If you prefer `npm`, you can do `npm run cypress open` instead of `yarn run cypress open`.
52 |
53 | ## Built With
54 |
55 | - [Blue](https://cruip.com/blue/) - Landing page template from [Cruip](https://cruip.com/).
56 | - [Next.js](https://nextjs.org/) - The React.js framework for building SSR web apps.
57 | - [React](https://facebook.github.io/react/) - A JavaScript library for building user interfaces
58 | - [Screely](https://www.screely.com/) - Instantly turn your screenshot into a beautiful design mockup
59 | - [Cypress](https://www.cypress.io/) - Fast, easy and reliable testing for anything that runs in a browser.
60 |
61 | ## Deploying
62 |
63 | 1. Create a ZEIT account at https://zeit.co/signup and download [the CLI](https://zeit.co/download)
64 | 2. Add the API key as a secret `now secrets add butter-cms-api-key "YOUR_API_KEY"`
65 | 3. Run `now` at the project root
66 |
67 | ## License
68 |
69 | MIT Licensed. Copyright (c) ButterCMS 2019.
70 |
--------------------------------------------------------------------------------
/src/components/pricing.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function Pricing({ data }) {
5 | return (
6 |
7 |
8 |
9 |
10 | {data.fields.title}
11 |
12 |
13 |
14 | {data.fields.pricing.map((preset, index) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | {preset.title}
22 |
23 |
24 |
25 | $
26 |
27 |
28 | {preset.price}
29 |
30 | /m
31 |
32 |
33 |
34 | {preset.features}
35 |
36 |
37 |
45 |
46 |
47 | );
48 | })}
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | Pricing.propTypes = {
58 | data: PropTypes.object.isRequired
59 | };
60 |
61 | export default Pricing;
62 |
--------------------------------------------------------------------------------
/src/assets/images/feature-icon-04.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/hubspot-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/facebook-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/blogs.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import Head from "next/head";
3 | import Butter from "buttercms";
4 |
5 | import Blogs from "../src/components/blogs";
6 | import Contact from "../src/components/contact";
7 | import Loader from "../src/assets/images/loader.svg";
8 | import Footer from "../src/components/footer";
9 | import Header from "../src/components/header";
10 |
11 | const ButterCMSContext = React.createContext();
12 | const butter = Butter(process.env.BUTTER_CMS_API_KEY);
13 |
14 | class BlogsPage extends Component {
15 | data = {
16 | loading: true,
17 | generalData: {},
18 | blogsData: {},
19 | contactData: {}
20 | };
21 |
22 | static async getInitialProps() {
23 | try {
24 | const response = await Promise.all([
25 | butter.page.retrieve("*", "general"),
26 | butter.post.list(),
27 | butter.page.retrieve("*", "contact")
28 | ]);
29 |
30 | this.data = {
31 | generalData: response[0].data.data,
32 | blogsData: response[1].data.data,
33 | contactData: response[2].data.data,
34 | loading: false
35 | };
36 | } catch (error) {
37 | console.error(error);
38 | }
39 |
40 | return {
41 | data: this.data
42 | };
43 | }
44 |
45 | render() {
46 | const { data } = this.props;
47 |
48 | return (
49 |
50 |
51 | Marketing Site using ButterCMS and Next.js
52 |
53 |
61 |
62 | {({ loading, generalData, blogsData, contactData }) => {
63 | if (loading)
64 | return (
65 |
66 |
67 |
78 |
79 | );
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | }}
90 |
91 |
92 |
93 | );
94 | }
95 | }
96 |
97 | export default BlogsPage;
98 |
--------------------------------------------------------------------------------
/src/components/footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Link from "next/link";
4 |
5 | function Footer({ data }) {
6 | return (
7 |
8 |
9 |
10 |
17 |
18 | {data.fields.navigation_links.map((link, index) => {
19 | return (
20 |
21 |
22 | {link.text}
23 |
24 |
25 | );
26 | })}
27 |
28 |
63 |
64 | © {new Date().getFullYear()} {data.fields.company_name}, all
65 | rights reserved
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | Footer.propTypes = {
74 | data: PropTypes.object.isRequired
75 | };
76 |
77 | export default Footer;
78 |
--------------------------------------------------------------------------------
/cypress/integration/index-page.js:
--------------------------------------------------------------------------------
1 | describe("Index page", () => {
2 | beforeEach(() => {
3 | cy.log(`Visiting http://localhost:3000`);
4 | cy.visit("/");
5 | });
6 |
7 | /**
8 | * Header section
9 | */
10 | it("should have a logo", () => {
11 | cy.get(".brand.header-brand img").should("have.length", 1);
12 | });
13 |
14 | /**
15 | * Hero section
16 | */
17 | it("should have a hero section with a title, description and a button", () => {
18 | cy.get(".hero .hero-title").should("have.length", 1);
19 | cy.get(".hero .hero-paragraph").should("have.length", 1);
20 | cy.get(".hero .hero-cta > .button").should("have.length", 1);
21 | });
22 |
23 | /**
24 | * Clients section
25 | */
26 | it("should have a clients section with two images", () => {
27 | cy.get(".clients img").should("have.length", 2);
28 | });
29 |
30 | /**
31 | * Features section
32 | */
33 | it("should have a features section with a title, description and four cards with an image, title and description", () => {
34 | cy.get(".features .section-title").should("have.length", 1);
35 | cy.get(".features .section-paragraph").should("have.length", 1);
36 | cy.get(".features .feature").should("have.length", 4);
37 | cy.get(".features .feature .feature-icon").should("have.length", 4);
38 | cy.get(".features .feature .feature-title").should("have.length", 4);
39 | cy.get(".features .feature .text-sm").should("have.length", 4);
40 | });
41 |
42 | /**
43 | * Testimonials section
44 | */
45 | it("should have a testimonials section with a title and two cards with a description and author name", () => {
46 | cy.get(".testimonials .section-title").should("have.length", 1);
47 | cy.get(".testimonials .testimonial").should("have.length", 2);
48 | cy
49 | .get(".testimonials .testimonial .testimonial-body")
50 | .should("have.length", 2);
51 | cy
52 | .get(".testimonials .testimonial .testimonial-name")
53 | .should("have.length", 2);
54 | });
55 |
56 | /**
57 | * Pricing section
58 | */
59 | it("should have a pricing section with a title and one card with a description, price and a CTA", () => {
60 | cy.get(".pricing .section-title").should("have.length", 1);
61 | cy.get(".pricing .pricing-table").should("have.length", 1);
62 | cy
63 | .get(".pricing .pricing-table .pricing-table-title")
64 | .should("have.length", 1);
65 | cy
66 | .get(".pricing .pricing-table .pricing-table-price")
67 | .should("have.length", 1);
68 | cy
69 | .get(".pricing .pricing-table .pricing-table-cta")
70 | .should("have.length", 1);
71 | });
72 |
73 | /**
74 | * FAQs section
75 | */
76 | it("should have a FAQs section with a title and two FAQs", () => {
77 | cy.get(".pricing .pricing-faqs h4").should("have.length", 1);
78 | cy.get(".pricing .pricing-faqs .accordion li").should("have.length", 2);
79 | });
80 |
81 | /**
82 | * CTA section
83 | */
84 | it("should have a CTA section with a title and a button", () => {
85 | cy.get(".cta .section-title").should("have.length", 1);
86 | cy.get(".cta .cta-cta").should("have.length", 1);
87 | });
88 |
89 | /**
90 | * Footer section
91 | */
92 | it("should have a footer section with a logo and three social links", () => {
93 | cy.get(".site-footer .brand img").should("have.length", 1);
94 | cy.get(".site-footer .footer-social-links li").should("have.length", 3);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/src/assets/js/main.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | const doc = document.documentElement;
3 |
4 | doc.classList.remove("no-js");
5 | doc.classList.add("js");
6 |
7 | // Reveal animations
8 | if (document.body.classList.contains("has-animations")) {
9 | /* global ScrollReveal */
10 | const sr = (window.sr = ScrollReveal());
11 |
12 | sr.reveal(".hero-title, .hero-paragraph, .hero-cta", {
13 | delay: 150,
14 | duration: 1000,
15 | distance: "60px",
16 | easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
17 | origin: "bottom",
18 | interval: 150
19 | });
20 |
21 | sr.reveal(".hero-right-decoration", {
22 | duration: 1000,
23 | distance: "40px",
24 | easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
25 | origin: "top"
26 | });
27 |
28 | sr.reveal(".hero-left-decoration", {
29 | duration: 1000,
30 | distance: "40px",
31 | easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
32 | origin: "bottom"
33 | });
34 |
35 | sr.reveal(".clients li", {
36 | delay: 300,
37 | duration: 1000,
38 | rotate: {
39 | y: 50
40 | },
41 | easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
42 | interval: 150
43 | });
44 |
45 | sr.reveal(
46 | ".feature, .tabs-links li, .testimonial, .pricing-table, .pricing-faqs, .cta-inner",
47 | {
48 | duration: 600,
49 | distance: "40px",
50 | easing: "cubic-bezier(0.215, 0.61, 0.355, 1)",
51 | interval: 100,
52 | origin: "bottom",
53 | viewFactor: 0.2
54 | }
55 | );
56 | }
57 |
58 | // Accordion component
59 | const accordionEl = document.getElementsByClassName("accordion-title");
60 |
61 | if (accordionEl.length) {
62 | for (let i = 0; i < accordionEl.length; i++) {
63 | accordionEl[i].addEventListener("click", function() {
64 | this.parentNode.classList.toggle("is-open");
65 | const panel = this.nextElementSibling;
66 | if (panel.style.maxHeight) {
67 | panel.style.maxHeight = null;
68 | } else {
69 | panel.style.maxHeight = `${panel.scrollHeight}px`;
70 | }
71 | });
72 | }
73 | }
74 |
75 | // Tabs component
76 | const tabLinksAll = document.getElementsByClassName("tab-link");
77 |
78 | if (tabLinksAll.length) {
79 | for (let i = 0; i < tabLinksAll.length; i++) {
80 | tabLinksAll[i].addEventListener("click", function(e) {
81 | e.preventDefault();
82 | let tabLinksContainer = tabLinksAll[i].parentNode.parentNode;
83 | let tabPanels = tabLinksContainer.nextElementSibling.getElementsByClassName(
84 | "tab-panel"
85 | );
86 | let tabLinks = tabLinksContainer.getElementsByClassName("tab-link");
87 | // Remove is-active class from all links and panels
88 | for (let i = 0; i < tabLinks.length; i++) {
89 | tabLinks[i].classList.remove("is-active");
90 | }
91 | for (let i = 0; i < tabPanels.length; i++) {
92 | tabPanels[i].classList.remove("is-active");
93 | }
94 | // Get the ID of panel to display
95 | let tabID = this.getAttribute("href");
96 | // Add is-active class to matching link and panel
97 | tabLinksAll[i].classList.add("is-active");
98 | document.querySelector(tabID).classList.add("is-active");
99 | });
100 | }
101 | }
102 | })();
103 |
--------------------------------------------------------------------------------
/src/assets/images/airbnb-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/microsoft-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import Head from "next/head";
3 | import Butter from "buttercms";
4 |
5 | import Clients from "../src/components/clients";
6 | import Contact from "../src/components/contact";
7 | import Features from "../src/components/features";
8 | import Footer from "../src/components/footer";
9 | import Header from "../src/components/header";
10 | import Hero from "../src/components/hero";
11 | import Pricing from "../src/components/pricing";
12 | import Testimonials from "../src/components/testimonials";
13 | import FAQ from "../src/components/faq";
14 | import Loader from "../src/assets/images/loader.svg";
15 |
16 | const ButterCMSContext = React.createContext();
17 | const butter = Butter(process.env.BUTTER_CMS_API_KEY);
18 |
19 | class IndexPage extends Component {
20 | data = {
21 | loading: true,
22 | heroData: {},
23 | clientsData: {},
24 | featuresData: {},
25 | testimonialsData: {},
26 | contactData: {},
27 | generalData: {},
28 | faqsData: {}
29 | };
30 |
31 | static async getInitialProps() {
32 | try {
33 | const response = await Promise.all([
34 | butter.page.retrieve("*", "hero"),
35 | butter.page.retrieve("*", "clients"),
36 | butter.page.retrieve("*", "features"),
37 | butter.page.retrieve("*", "testimonials"),
38 | butter.page.retrieve("*", "pricing"),
39 | butter.page.retrieve("*", "contact"),
40 | butter.page.retrieve("*", "general"),
41 | butter.page.retrieve("*", "faqs")
42 | ]);
43 |
44 | this.data = {
45 | heroData: response[0].data.data,
46 | clientsData: response[1].data.data,
47 | featuresData: response[2].data.data,
48 | testimonialsData: response[3].data.data,
49 | pricingData: response[4].data.data,
50 | contactData: response[5].data.data,
51 | generalData: response[6].data.data,
52 | faqsData: response[7].data.data,
53 | loading: false
54 | };
55 | } catch (error) {
56 | console.error(error);
57 | }
58 |
59 | return {
60 | data: this.data
61 | };
62 | }
63 |
64 | render() {
65 | const { data } = this.props;
66 |
67 | return (
68 |
69 |
70 | Marketing Site using ButterCMS and Next.js
71 |
72 |
85 |
86 | {({
87 | loading,
88 | heroData,
89 | clientsData,
90 | featuresData,
91 | testimonialsData,
92 | pricingData,
93 | contactData,
94 | generalData,
95 | faqsData
96 | }) => {
97 | if (loading)
98 | return (
99 |
100 |
101 |
112 |
113 | );
114 |
115 | return (
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | );
128 | }}
129 |
130 |
131 |
132 | );
133 | }
134 | }
135 |
136 | export default IndexPage;
137 |
--------------------------------------------------------------------------------
/src/assets/images/header-bg-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/tinder-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/header-bg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/header-bg-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------