gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.23k stars 10.32k forks source link

How to call API script (from Dribble) into my Gatsby Site #15603

Closed judepark closed 5 years ago

judepark commented 5 years ago

Originally Posted on: https://stackoverflow.com/questions/56974242/how-to-add-api-script-from-dribble-into-my-gatsby-site


I am trying to call my Dribble posts onto my Gatsby website.

I followed the tutorial by this article https://medium.com/@nithin_94885/dribbble-shots-in-your-website-v2-api-5945a355d106, where I generated my Access Token from Dribble (a unique code to allow my gatsby site to access my dribble posts).

I am having trouble figuring out where to add the script that calls the API in my gatsby site.

I have tried pasting it to my gatsby-node.js file (In the past I thought this was where the script should go)

I am quite the newbie.. if the script that calls the Dribble API looks like this:

// Set the Access Token
var accessToken = '9f061d26c5a8be96b17a81718959a67dd54ca9669ca41752777193f7cc5be7c3';

// Call Dribble v2 API
$.ajax({
    url: 'https://api.dribbble.com/v2/user/shots?access_token='+accessToken,
    dataType: 'json',
    type: 'GET',
    success: function(data) {  
      if (data.length > 0) { 
        $.each(data.reverse(), function(i, val) {                
          $('#shots').prepend(
            '<a class="shot" target="_blank" href="'+ val.html_url +'" title="' + val.title + '"><div class="title">' + val.title + '</div><img src="'+ val.images.hidpi +'"/></a>'
            )
        })
      }
      else {
        $('#shots').append('<p>No shots yet!</p>');
      }
    }
});

and my gatsby-node.js file looks like this:

const path = require('path');
const { createFilePath } = require('gatsby-source-filesystem');

// Look at every node when it is created
exports.onCreateNode = ({node, getNode, actions}) => {
    // Check for markdown nodes
    const { createNodeField } = actions;
    if(node.internal.type === 'MarkdownRemark') {
        // Create a slug out of the markdown filepath name
        const slug = createFilePath({
            node,
            getNode,
            basePath: 'projects'
        });
        // Add the newly created slug to the node itself
        createNodeField({
            node,
            name: 'slug',
            value: `/project${slug}`
        });
    }
};

exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions
    return new Promise((resolve, reject) => {
      graphql(`
        {
          allMarkdownRemark {
            edges {
              node {
                fields {
                  slug
                }
              }
            }
          }
        }
      `).then(result => {
        result.data.allMarkdownRemark.edges.forEach(({ node }) => {
          createPage({
            path: node.fields.slug,
            component: path.resolve(`./src/templates/project.js`),
            context: {
              // Data passed to context is available in page queries as GraphQL variables.
              slug: node.fields.slug,
            },
          })
        })
        resolve()
      })
    })
    };

How can I add the script so that the API script will call my shots in Dribble to link back to it?

I expected this to be an easy solution but I have been struggling with this Dribble/Gatsby integration for days now. :(

jonniebigodes commented 5 years ago

@judepark i'm going to create a demo with a couple of approaches for you and let you decide how you want to go from there. Can you wait a bit while i implement this and come back and provided a detailed explanation?

judepark commented 5 years ago

Yes absolutely! thank you!

jonniebigodes commented 5 years ago

@judepark sorry for the delay, but i was otherwise engaged. Below are the steps i took to try and solve your issue based on the information at hand.

module.exports = { / Your site config here / plugins:[ { resolve: gatsby-source-filesystem, options: { path: ${__dirname}/content/projects, name: projects, }, }, gatsby-transformer-remark

] }

Nothing to complicated here, just configured `gatsby-source-filesystem` to look for content in the `/content/projects` folder. And "activated" `gatsby-transformer-remark`.

- Opened a new browser window to the endpoint you provided in your description and saw that some data was returned from it and based on that i've added the following folder structure. I don't know if that's your data. But if it's not the rest of this will at least provide you with a general direction to follow.

|project-root |content |projects |2-Dribbble-Invites index.md |Animated-Reserved-Toggle index.md |Limo-Hire-App-Concept index.md |Lynx-App-Icon index.md |Lynx-Branding index.md | Lynx-Limo-Hire-Brand index.md | S-Mark-Grid index.md |Sliding-S-Animation index.md |SPBranding index.md |The-Brush-Lady-Website index.md |The-New-Order-Branding index.md |Website-Scrolling-Animations index.md

 - To keep it simple each `index.md` has the following structure. For instance `2-Dribbble-Invites/index.md` content is the following:
```markdown
---
title: 2 Dribbble Invites
---

this is random data for 2 Dribbble Invites

// api hook to create pages with async/await // more on that below // https://www.gatsbyjs.org/docs/creating-and-modifying-pages/ // https://www.gatsbyjs.org/tutorial/part-seven/

exports.createPages = async ({ actions, graphql }) => { const { createPage } = actions // uses axios to fetch the data from dribble const axiosRequest = await axios( https://api.dribbble.com/v2/user/shots?access_token=${process.env.DRIBBLE_API_TOKEN} )

// destructure the axios data prop to get the result from the request made above const { data } = axiosRequest

// gets the markdown data needed const markdDownPages = await graphql( { allMarkdownRemark { edges { node { id html fields { slug } frontmatter { title } html } } } } ) if (markdDownPages.errors) { throw new Error(markdDownPages.errors) }

// iterates over the results fetched and creates the page based on that data markdDownPages.data.allMarkdownRemark.edges.forEach(edge => { // gets the index from the result from dribble api request const dribblePosition = data.findIndex( x => x.title === edge.node.frontmatter.title )

/**
 * invokes createpage api hook to create a page
 * path:based on the slug retrieved i.e http://localhost:8000/2-Dribbble-Invites/
 * component: the template used 
 * context: data that will passed to the page internally
 * projectData: is the data coming from the graphql data
 * dribbleData: is the data from dribble 
 */
createPage({
  path: `${edge.node.fields.slug}`,
  component: require.resolve("./src/templates/ProjectTemplate.js"),
  context: {
      projectData:{
          id:edge.node.id,
          title:edge.node.frontmatter.title,
          content:edge.node.html
      },
      dribbleData:data[dribblePosition]
  },
})

}) }

// api hook to extend the node created // in this case the nodes relative to markdown data // more on that https://www.gatsbyjs.org/docs/node-apis/#onCreateNode exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions if (node.internal.type === MarkdownRemark) { const value = createFilePath({ node, getNode }) createNodeField({ name: slug, node, value, }) } }

- Created a .env file with the following content:

DRIBBLE_API_TOKEN=9f061d26c5a8be96b17a81718959a67dd54ca9669ca41752777193f7cc5be7c3

Now instead of leaving the api key directly in your code, it will be read from a environment variable loaded with `dotenv` package from the file.

- Created the template under `/src/templates/ProjectTemplate.js` with the following code inside:
```javascript
import React from "react"

// basic functional component with the gatsby special prop `pageContext` already destructured
const ProjectTemplate = ({ pageContext }) => {
  // destructure both properties that were passed in via gatsby-node.js
  const { projectData, dribbleData } = pageContext

  return (
    <div
      style={{
        margin: "0 auto",
      }}
    >
      <div>
        <h1>{projectData.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: projectData.content }} />
        <a
          href={`${dribbleData.html_url}`}
          title={dribbleData.title}
          target="_noopener"
          rel="nofollow"
        >
          <img
            style={{ width: "220px", height: "220px" }}
            src={`${dribbleData.images.hidpi}`}
            alt={`${dribbleData.title}`}
          />
        </a>
      </div>
      <div
        style={{
          fontFamily: "monospace",
          display: "block",
          padding: "10px 30px",
          margin: "0",
        }}
      >
        <h3>Dribble Data passsed via page context</h3>
        <pre>{JSON.stringify(dribbleData, null, 2)}</pre>
      </div>
    </div>
  )
}

export default ProjectTemplate

As you can see nothing complicated, this template will use the data passed via Gatsby's special prop pageContext so that you can see the data as soon as you hit the page.

- Issued `gatsby develop` and waited for the process to complete and opening up a browser window to `http://localhost:8000` i'm presented with the following:

![dribble_1](https://user-images.githubusercontent.com/22988955/61064371-87151200-a3f9-11e9-9ed7-17f681344658.png)
- Clicked a random item and i'm presented with the following:

![dribble_2](https://user-images.githubusercontent.com/22988955/61064524-dce9ba00-a3f9-11e9-90c1-27583a275517.png)

Below i'll document a alternative approach to your issue.
- Modified `gatsby-node.js` `createPages` api call  for the following:
```javascript
exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions

  // gets the markdown data needed
  const markdDownPages = await graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            id
            html
            fields {
              slug
            }
            frontmatter {
              title
            }
            html
          }
        }
      }
    }
  `)
  if (markdDownPages.errors) {
    throw new Error(markdDownPages.errors)
  }

  // iterates over the results fetched and creates the page based on that data
  markdDownPages.data.allMarkdownRemark.edges.forEach(edge => {
    /**
     * invokes createpage api hook to create a page for version 2
     * path:based on the slug retrieved i.e http://localhost:8000/v2/2-Dribbble-Invites/
     * component: the template used 
     * context: data that will passed to the page internally
     * projectData: is the data coming from the graphql data
     */
    createPage({
      path: `/v2${edge.node.fields.slug}`,
      component: require.resolve("./src/templates/ProjectTemplate_v2.js"),
      context: {
          projectData:{
              id:edge.node.id,
              title:edge.node.frontmatter.title,
              content:edge.node.html
          },
      },
    })
  })
}

In this case the only thing that will be passed to the page itself will be the result of the graphl query.

export default ProjectTemplate_v2


- Created a new page called `page-2.js` under `src/pages/` with the following content:

```javascript
import React, { Component } from "react"
import axios from "axios"
import { graphql, Link } from "gatsby"

/**
 * this page will handle fetching the dribble data and store it in it's state
 */
class Page2 extends Component {
  state = {
    dribbleData: [],
    isError: false,
  }

  // component livecycle call
  async componentDidMount() {
    try {
      // this time the environment variable will use fetched depending on your mode(development or production) from .env.development or .env.production
      // more on that here https://www.gatsbyjs.org/docs/environment-variables/
      const datafromDribble = await axios(
        `https://api.dribbble.com/v2/user/shots?access_token=${process.env.GATSBY_DRIBBLE_API_TOKEN}`
      )
      const { data } = datafromDribble
      this.setState({ dribbleData: data })
    } catch (error) {
      console.log("====================================")
      console.log(`something went wrong:${error}`)
      console.log("====================================")
      this.setState({ isError: true })
    }
  }
  render() {
    const { dribbleData, isError } = this.state
    const { data } = this.props
    if (isError) {
      return (
        <>
          <h1>Something went wrong</h1>
          <h4>try reloading the page</h4>
        </>
      )
    }
    return (
      <div>
        <h1>List of pages version 2.0</h1>
        <ul>
          {data.allMarkdownRemark.edges.map(edge => {
            return (
              <li key={edge.node.id}>
                {/* creates a new gatsby link to the page created in gatsby-node.js (v2) and injects via link state prop the data regarding that page */}
                <Link to={`/v2${edge.node.fields.slug}`} state={{dribleItem:dribbleData.filter( x => x.title === edge.node.frontmatter.title)}}>
                  {edge.node.frontmatter.title}
                </Link>
              </li>
            )
          })}
        </ul>
      </div>
    )
  }
}
// a page query identical to the one used for v1
// more on that here https://www.gatsbyjs.org/docs/page-query/
export const query = graphql`
  {
    allMarkdownRemark {
      edges {
        node {
          id
          fields {
            slug
          }
          frontmatter {
            title
          }
        }
      }
    }
  }
`
export default Page2

dribble_3

Now for some things regarding the approaches i took. Both approaches documented here offer you some insight on how you can achieve what you want to with Gatsby.

It's entirely up to you on which route you want to follow.

Using the latter one might yeld you some issues if you don't modify the code accordingly. Probably instead of passing the whole object via state prop on page-2 to the pages that were created dynamically you might want to pass the dribble id for instance, or take another diferent approach so that when you share the page in a social media you'll actually get the dribble data, or when you open a browser window to a specific page.

On a personal note i would go with the first route. so that i can get the data from dribble during the build and when i hit a random page i have it already available. And with that leave only the pages and templates to render what i want to show on the website. In a nutshell a separation of concerns. But once again it's up to you to choose what approach you want to take.

Now if you want i can put the code in a repository so that you can look at it in more detail and at your own pace. Just let me know.

And if you don't mind, sorry for the extremely long comment.

Feel free to provide feedback so that we can close this issue or continue to work on it till we find a suitable solution.

judepark commented 5 years ago

Firstly thank you so much for such a detailed step-by-step guide. I am working on recreating the environment to test for myself, if nothing else the steps you provided is helping me become more acquainted with calling API's in Gatsby, as well as axios, dotenv, which I am not acquainted with.

I am wondering how I can call my Dribble shots not as individual "blog" or "ProjectTemplate" posts, but to simply have the images of my dribble shots show up as clickable links that can then navigate the user to my original Dribble posts. This here is an example by Matthew Elsom: https://codepen.io/matthewelsom/pen/aVLyoO

I can tweak the css on my own, but I am wondering if there is a simpler way to integrate the JS script from the Matthew Elsom's example into my Gatsby site. If not, then I am right in assuming that Dribble API integration with Gatsby will require more work as you have suggested. I apologize if I am wrong in assuming there is an easier solution, either way the step by step guide you have provided is helping me become better acquainted with how Gatsby works!

jonniebigodes commented 5 years ago

@judepark i'm going to expand my reproduction and come back and detail the steps i took. Do you mind waiting just a bit longer?

judepark commented 5 years ago

Absolutely, thank you!!

jonniebigodes commented 5 years ago

@judepark going to expand my previous comment so that it adjusts to what you mentioned in your previous comment. I've checked the codepen you supplied and adjusted accordingly.

exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions
  // uses axios to fetch the data from dribble
  const axiosRequest = await axios(
    `https://api.dribbble.com/v2/user/shots?access_token=${process.env.DRIBBLE_API_TOKEN}`
  )

  // destructure the axios data prop to get the result from the request made above
  const { data } = axiosRequest

  // gets the markdown data needed
  const markdDownPages = await graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            id
            html
            fields {
              slug
            }
            frontmatter {
              title
            }
            html
          }
        }
      }
    }
  `)
  if (markdDownPages.errors) {
    throw new Error(markdDownPages.errors)
  }
  /**
   * invokes createpage api hook to create a page
   * path: http://localhost:8000/mydribbles/
   * component: the template used
   * context: data that will passed to the page internally
   * dribbles: is the data coming from the graphql data
   */
  createPage({
    path: "/mydribbles/",
    component: require.resolve("./src/templates/DribblesTemplate.js"),
    context: {
      dribbles: data,
    },
  })

  // iterates over the results fetched and creates the page based on that data
  markdDownPages.data.allMarkdownRemark.edges.forEach(edge => {

    /**
     * invokes createpage api hook to create a page
     * path:based on the slug retrieved i.e http://localhost:8000/2-Dribbble-Invites/
     * component: the template used
     * context: data that will passed to the page internally
     * projectData: is the data coming from the graphql data
     */
    createPage({
      path: `${edge.node.fields.slug}`,
      component: require.resolve("./src/templates/ProjectTemplate.js"),
      context: {
        projectData: {
          id: edge.node.id,
          title: edge.node.frontmatter.title,
          content: edge.node.html,
        },
      },
    })
  })
}

Comparing with my first approach i mentioned above. The changes here are the following. You're fetching the dribble data a priori like i mentioned before. But now instead of sending it to a page that is associated to a markdown post you're sending it to a page. The template ProjectTemplate was modified accordingly so that these changes are reflected. I'm leaving it out so that the comment won't become very big.

import React from "react"

import "../assets/css/dribbles.css"
// basic functional component with the gatsby special prop `pageContext` already destructured
const DribblesTemplate = ({ pageContext }) => {
  // destructure the propertie that were passed in via gatsby-node.js
  const { dribbles } = pageContext

  return (
    <div>
      <h1>dribbles via gatsby node</h1>
      <div className="shots">
        {dribbles.map(dribble => {
          return (
            <a
              className="shot"
              target="_noopener"
              href={dribble.html_url}
              rel="nofollow"
              title={dribble.title}
            >
              <div className="title">{dribble.title}</div>
              <img src={dribble.images.hidpi} alt={dribble.title} />
            </a>
          )
        })}
      </div>
    </div>
  )
}

export default DribblesTemplate

Nothing to different like before this component will recieve the data that is provided by Gatsby's special prop pageContext and it displays it. The css file is somewhat similar to the one supplied in the codepen you've supplied. I'm leaving it out for brevity purposes.

dribble_5

Alternatively you can move from this approach and move the dribble fetching to the "client" side of things. And i'm going to enumate to ways to achieve it, starting from a simple one.

import React from "react"
import MyDribbles_v1 from "../components/MyDribbles_v1"
const Page3 = () => (
  <div>
    <h1>this is a Gatsby page</h1>
    <div>
      <MyDribbles_v1 />
    </div>
  </div>
)

export default Page3

As you can see it's rather simple page. based on a functional React component,

import React, { Component } from "react"
import axios from "axios"
import '../assets/css/dribbles.css'

class MyDribbles_v1 extends Component {
  state = {
    isError: false,
    dribbles: [],
  }

  async componentDidMount() {
    try {
      // this time the environment variable will use fetched depending on your mode(development or production) from .env.development or .env.production
      // more on that here https://www.gatsbyjs.org/docs/environment-variables/
      const datafromDribble = await axios(
        `https://api.dribbble.com/v2/user/shots?access_token=${process.env.GATSBY_DRIBBLE_API_TOKEN}`
      )
      const { data } = datafromDribble
      this.setState({ dribbles: data })
    } catch (error) {
      console.log("====================================")
      console.log(`something went wrong:${error}`)
      console.log("====================================")
      this.setState({ isError: true })
    }
  }
  render() {
    const { isError, dribbles } = this.state

    if (isError) {
      return (
        <>
          <h1>Something went wrong</h1>
          <h4>try reloading the page</h4>
        </>
      )
    }
    return (
      <div className="shots">
        <h1>dribbles shown directly in a component responsible for fetching the data</h1>
        {dribbles.map(dribble => {
          return (
            <a
              className="shot"
              target="_noopener"
              href={dribble.html_url}
              rel="nofollow"
              title={dribble.title}
            >
              <div className="title">{dribble.title}</div>
              <img src={dribble.images.hidpi} alt={dribble.title} />
            </a>
          )
        })}
      </div>
    )
  }
}

export default MyDribbles_v1

In this case the component itself is responsible to fetch the data and display it.

dribble_6

import React, { Component } from "react"
import axios from "axios"
import MyDribbles_v2 from "../components/MyDribbles_v2"
class Page4 extends Component {
  state = {
    isError: false,
    dribbles: [],
  }

  async componentDidMount() {
    try {
      // this time the environment variable will use fetched depending on your mode(development or production) from .env.development or .env.production
      // more on that here https://www.gatsbyjs.org/docs/environment-variables/
      const datafromDribble = await axios(
        `https://api.dribbble.com/v2/user/shots?access_token=${process.env.GATSBY_DRIBBLE_API_TOKEN}`
      )
      const { data } = datafromDribble
      this.setState({ dribbles: data })
    } catch (error) {
      console.log("====================================")
      console.log(`something went wrong:${error}`)
      console.log("====================================")
      this.setState({ isError: true })
    }
  }
  render() {
    const { isError, dribbles } = this.state
    if (isError) {
      return (
        <>
          <h1>Something went wrong</h1>
          <h4>try reloading the page</h4>
        </>
      )
    }
    return(
      <div>
        <h1>this is another Gatsby page</h1>
        <div>
        {/* the dribble data that "lives" in the state will be passed down to the component as a prop */}
        <MyDribbles_v2 dribbles={dribbles}/>
        </div>
      </div>
    )
  }
}

export default Page4
import React from "react"
import '../assets/css/dribbles.css';

const MyDribbles_v2 = ({ dribbles }) => {
  return (
    <div className="shots">
      <h1>
        dribbles shown directly in a functional component with the data passed via React Props
      </h1>
      {dribbles.map(dribble => {
        return (
          <a
            className="shot"
            target="_noopener"
            href={dribble.html_url}
            rel="nofollow"
            title={dribble.title}
          >
            <div className="title">{dribble.title}</div>
            <img src={dribble.images.hidpi} alt={dribble.title} />
          </a>
        )
      })}
    </div>
  )
}

export default MyDribbles_v2

In this case instead the component be responsible to fetch and display the data, it will be the responsibility of the page, when it's visited.

dribble_7

As you can see you don't need to integrate the javascript file you mentioned. You can achieve the same result with a bit of work.

I've created a repo here with all of the code i mentioned here. So that you can go over it at your own pace and fiddle with it and see how things work.

Feel free to provide feedback so that we can close this issue or continue to work on it till we find a suitable solution.

Also one thing i forgot to mention, no need to thank, just glad i was able to help

judepark commented 5 years ago

I followed your instructions and was able to integration my Dribble content directly into my Gatsby site! I say this was a huge learning curve for me but I was able to get there in the end, thank you for creating the repo, it made the process that much easier to follow, I learn better by having files to work with directly. Thank you for helping me out through this, now I can showcase my dribbble shots on my website :)

jonniebigodes commented 5 years ago

@judepark again...no need to thank, i'm glad i was able to help out. If you don't mind i'm going to close this issue as it's now resolved and also should any other issues rise feel free to open a new issue. Thanks for using Gatsby 👍