Grants API – 5 Minute Matcher

This is a minimal HTML/CSS/JS application that creates a basic grants matcher in a few minutes. It uses a back-end-proxy.php file to make requests to the Grants API. You can see the application in action here: front-end.html

This method prevents your API key being exposed to the end-user and also allows you to do any filtering of results before using them in your front-end. For example, you may wish to only show certain Regions in the select box or filter out certain grant providers before presenting them to the user.

Grants API Example

First we have our basic front-end using HTML, CSS and JavaScript which displays two select fields, Sector and Region with a Submit button. The Sector and Region select options are populated from the Grants API via a fetch() to our back-end-proxy.php file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>5 Minute Matcher - A basic Grants API front-end</title>
    <!-- Allows us to use templates to display arrays of data. 
        We'll use it to display the purpose results from the API. -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/handlebars.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        color: #333333;
        margin: 20px;
        padding: 0;
      }
      header {
        background-color: #1c4b9b;
        color: #fff;
        padding: 10px;
      }
      main {
        display: flex;
        margin: 40px 20px;
        min-height: 600px;
      }
      .form-section {
        width: 200px;
        padding-right: 40px;
      }
      .results-section {
        flex: 1;
      }
      form {
        display: flex;
        flex-direction: column;
      }
      #form-error {
        color:red;
        font-size: .8em;
      }
      label {
        font-weight: bold;
        margin-bottom: 10px;
      }
      select {
        width: 100%;
        padding: 5px;
        margin-bottom: 10px;
      }
      button {
        margin-top: 20px;
        padding: 10px;
      }
      #result-summary {
        margin-bottom: 20px;
      }
      .result-item,
      .purpose-item {
        margin-bottom: 10px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 10px;
        background-color: #f9f9f9;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        transition: background-color 0.3s;
      }
      .result-item:hover {
        background-color: #eaf7eb;
        cursor: pointer;
      }
      .results-transition {
        transition: opacity 0.3s ease-in-out; /* Customize the duration and easing as desired */
        opacity: 1; /* Initial opacity value */
      }
      .results-transition.fade-out {
        opacity: 0; /* Opacity value when fading out */
      }
      .purpose-item {
        background-color: #eaf7eb;
      }
      .purpose-item h3 {
        text-align: center;
      }
      .purpose-item > a {
        color: blue;
        cursor: pointer;
        margin-bottom: 20px;
      }
      .purpose-item-row {
        display: flex;
        margin-bottom: 5px;
      }
      .purpose-item-row > div:first-of-type {
        width: 30%;
        text-align: right;
        margin-right: 10px;
      }
      .purpose-item-row > div:nth-of-type(2) {
        width: 60%;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>5 Minute Matcher - A basic Grants API front-end</h1>
    </header>

    <p>
      This is a basic/minimal HTML/CSS/JS front end that makes requests to a
      back-end-proxy.php file. It first makes two GraphQL calls to get the
      Sector and Region choices then, once the form is submitted, it requests the
      results. When a result is clicked it then makes a further call to fetch
      the purpose details to display to the user.
        <ul>
            <li>This is a minimal example and doesn't do the full validation and
        error handling that would normally be present.</li>
            <li>The back-end-proxy.php file uses the test API key so it will always return the same 2 Purpose results regardless of the Sector and Region passed in.</li>
            <li>Once you replace the test API key with your live one then real results will be returned.</li>
        </ul>
    </p>

    <main>
      <!-- A basic form which display just two of the possible select options-->
      <section class="form-section">
        <form id="grant-form" method="GET" action="">
          <label for="select_sector">Sector:</label>
          <select id="select_sector">
            <option value="">--Select a Sector--</option>
          </select>
          <label for="select_region">Region:</label>
          <select id="select_region">
            <option value="">--Select a Region--</option>
          </select>
          <div id="form-error"></div>
          <button type="submit">Submit</button>
        </form>
      </section>
      <!-- The area to display the results and purpose profile -->
      <section class="results-section">
        <div id="results" class="results-transition"></div>
      </section>
    </main>

    <!-- A template to display the purpose results-->
    <script id="result-template" type="text/template">

      <!-- First display some summary totals -->
      <div id="result-summary">
          <div>Total Results: {{allPurposes.totalResults}}</div>
          <div>Total Pages: {{allPurposes.totalPages}}</div>
      </div>

      <!-- Loop over the purpose results displaying whatever fields we choose -->
      {{#each allPurposes.results}}
          <div class="result-item" id="result-item-{{id}}" onclick="fetchPurpose('{{id}}')">
              <h3>{{purpose_title}}</h3>
              <h4>{{funding_purpose}}</h4>
              <p>{{purpose_description}}</p>

              {{#if purpose_size_max_formatted}}
                <div>Amount Available: {{grant.currency}}{{purpose_size_max_formatted}}</div>
              {{/if}}

              {{#if grant.provider_1}}
                <div>Provided by: {{grant.provider_1}}</div>
              {{/if}}
          </div>
      {{/each}}
    </script>

    <!-- Template to display a purpose profile when it's clicked on the results page -->
    <script id="purpose-template" type="text/template">
      <div class="purpose-item">
          <a onclick="showResults()">&#8592; Back to results</a>
          <h3>{{purpose_title}}</h3>

          {{#if funding_purpose}}
              <div class="purpose-item-row">
                  <div>Funding Purpose:</div>
                  <div>{{funding_purpose}}</div>
              </div>
          {{/if}}

          {{#if purpose_size_max_formatted}}
              <div class="purpose-item-row">
                  <div>Maximum Amount Available:</div>
                  <div>{{grant.currency}}{{purpose_size_max_formatted}}</div>
              </div>
          {{/if}}

          {{#if purpose_dates}}
              <div class="purpose-item-row">
                  <div>Deadlines:</div>
                  <div>{{purpose_dates}}</div>
              </div>
          {{/if}}

          {{#if sectors}}
              <div class="purpose-item-row">
                  <div>Sectors:</div>
                  <div>
                      {{#each sectors}}
                          {{sector}}{{#unless @last}}, {{/unless}}
                      {{/each}}
                  </div>
              </div>
          {{/if}}

          {{#if regions}}
              <div class="purpose-item-row">
                  <div>Regions:</div>
                  <div>
                      {{#each regions}}
                          {{display_region}}{{#unless @last}}, {{/unless}}
                      {{/each}}
                  </div>
              </div>
          {{/if}}

          {{#if purpose_url}}
              <div class="purpose-item-row">
                  <div>Find out more:</div>
                  <div><a href="{{purpose_url}}" title="Find out more" target="_blank">{{purpose_url}}</a></div>
              </div>
          {{/if}}

          {{#if purpose_application_url}}
              <div class="purpose-item-row">
                  <div>Apply now:</div>
                  <div><a href="{{purpose_application_url}}" title="Apply now" target="_blank">{{purpose_application_url}}</a></div>
              </div>
          {{/if}}
      </div>
    </script>

    <script>
      //Our back end proxy script that will interact with the Grants API
      const backEndUrl = "back-end-proxy.php";

      //First fetch the Sector and Region select choices
      Promise.all([
        fetch(backEndUrl + "?action=fetchSectors").then((response) =>
          response.json()
        ),
        fetch(backEndUrl + "?action=fetchRegions").then((response) =>
          response.json()
        ),
      ])
        .then((data) => {
          const [sectorsData, regionsData] = data;

          const selectSector = document.getElementById("select_sector");
          const selectRegion = document.getElementById("select_region");

          // Create a select option for each of the sectorsData
          sectorsData["allSectors"].forEach((item) => {
            const option = document.createElement("option");
            option.value = item.sector;
            option.textContent = item.sector;
            selectSector.appendChild(option);
          });

          // Create a select option for each of the regionsData
          regionsData["allRegions"].forEach((item) => {
            const option = document.createElement("option");
            option.value = item.region;
            option.textContent = item.region;
            selectRegion.appendChild(option);
          });
        })
        .catch((error) => {
          console.error("Error:", error);
        });

      /*This will store the purpose results list HTML so we can restore it when
       * the user clicks 'Back to results'
       */
      let generatedHTML = "";

      /**
       * This handles the Submit button. It sends the selected Sector and Region to the back end and receives
       * a list of purposes which it passes to our results template which loops over them and displays each one
       */

      document
        .getElementById("grant-form")
        .addEventListener("submit", function (event) {
          event.preventDefault(); // Prevent the default form submission

          // Get the selected values from the form
          const selectedSector = document.getElementById("select_sector").value;
          const selectedRegion = document.getElementById("select_region").value;

          if(selectedSector === "" && selectedRegion === "") {
            document.getElementById('form-error').innerHTML = 'Please select a Sector and/or Region';
          } else {
            document.getElementById('form-error').innerHTML = '';
            // Construct the URL with the selected values
            const url =
              backEndUrl +
              `?action=fetchResults&sector=${selectedSector}&region=${selectedRegion}`;

            // Perform the GET request to our back end
            fetch(url)
              .then((response) => response.json())
              .then((data) => {
                //Get our results template
                const resultTemplate =
                  document.getElementById("result-template").innerHTML;
                //Compile it using handlebars.js
                const compiledTemplate = Handlebars.compile(resultTemplate);
                //Get the generated HTML
                generatedHTML = compiledTemplate(data);
                // Display the generated HTML in the results element
                document.getElementById("results").innerHTML = generatedHTML;
              })
              .catch((error) => {
                console.error("Error:", error);
              });
          }
        });

      /**
       * When a user clicks on a purpose result we fetch the full purpose details
       * from the back and display them. We store the original purpose results HTML so we can display
       * it when the user clicks 'Back to results'
       */
      function fetchPurpose(id) {
        //Construct the back end request URL
        const furl = backEndUrl + "?action=fetchPurpose&id=" + id;
        fetch(furl)
          .then((response) => response.json())
          .then((data) => {
            //Get our template that displays a single purpose
            const purposeTemplate =
              document.getElementById("purpose-template").innerHTML;

            //Compile it using handlebars
            const compiledTemplate = Handlebars.compile(purposeTemplate);
            const generatedHTML = compiledTemplate(data.purpose);

            //Store the original HTML so we can restore it when a user clicks 'Back to results'
            const originalHTML = document.getElementById("results").innerHTML;

            //So the displaying of the purpose profile isn't too jarring we'll present it using a simple fade
            const resultsElement = document.getElementById("results");
            resultsElement.classList.add("fade-out");

            setTimeout(function () {
              resultsElement.innerHTML = generatedHTML;
              resultsElement.classList.remove("fade-out");
            }, 300);
          })
          .catch((error) => {
            console.error("Error:", error);
          });
      }

      /* When a user clicks 'Back to results' we can simply replace the purpose profile
       * HTML with the purpose results list HTML we saved previously
       */
      function showResults() {
        document.getElementById("results").innerHTML = generatedHTML;
      }
    </script>
  </body>
</html>

Here we have our back-end-proxy.php file that handles the form submissions and makes requests to the Grants API.

<?php

/**
 * A basic back end proxy file that handles the incoming requests and
 * performs the GraphQl queries to the Grants API
 * 
 * Note: This is a basic example and doesn't perform the usual 
 * input/output validation that you would normally do
 */

//The URL of the Grants API
define('API_URL', 'https://api.xsortal.com:4001');

/**
 * What is our API key - using this proxy PHP file prevents revealing the key to the end user
 * The test API key will return the same 2 Purpose results no matter the query.
 * When you plug in your live API key you'll get the 'real' results and have full control
 * over the offset (currentPage) and limit (perPage) parameters that you can use for pagination.
 */
define('API_KEY', '123456789abcdefg');

/**
 * The list of actions allowed in the GET request from the front end
 */
define('ALLOWED_ACTIONS', [
    'fetchSectors',
    'fetchRegions',
    'fetchResults',
    'fetchPurpose'
]);

/**
 * Check the requested action is an allowed one and create the relevant GraphQL query 
 * for each action.
 * 
 * For simplicity we make individual calls to fetch the Sectors and Regions. 
 * But GraphQL does allow you to batch requests into one, for example:
 * 
 * query allSectorsAndRegions {
 *  allSectors {
 *      sector
 *  }
 *  allRegions {
 *      display_region
 *  }
 *}

 */
if (!empty($_GET['action']) && in_array($_GET['action'], ALLOWED_ACTIONS)) {
    switch ($_GET['action']) {
            //Fetch the list of Sectors
        case 'fetchSectors':
            /**
             * Construct the GraphQL query to fetch the possible list of Sectors
             * 
             * * This query has no impact on our API limits
             * See: https://apidocs.getbusinessgrants.com/#query-allSectors
             */
            $query = '
                query {
                    allSectors {
                        sector
                    }
                }
                ';
            break;
            //Fetch the list of Regions
        case 'fetchRegions':
            /**
             * Construct the GraphQL query to fetch the possible list of Regions
             * 
             * This query has no impact on our API limits
             * See: https://apidocs.getbusinessgrants.com/#query-allRegions
             */
            $query = '
                    query {
                        allRegions {
                            region
                        }
                    }
                    ';
            break;
            //Fetch a page of results
        case 'fetchResults':
            /**
             * Get the passed in Sector and Region otherwise we'll just set some defaults here
             */
            $sector = !empty($_GET['sector']) ? $_GET['sector'] : 'Construction';
            $region = !empty($_GET['region']) ? $_GET['region'] : 'South West';

            /**
             * Construct the GraphQL query to fetch the list of purpose results
             * 
             * This query will use 1 of our allowed API limits per month the first time it is performed (unless you're on the unlimited package)
             * If this query is performed again (per month), using the exact same Sector and Region, then it will be a non-unique query and will not use and of the API limits (only unique queries impact the usage limits)
             * See: https://apidocs.getbusinessgrants.com/#query-allPurposes
             */
            $query = '
                query {
                    allPurposes(
                        sector: "' . $sector . '"
                        region: "' . $region . '"
                        perPage: 5
                        currentPage: 1
                        orderBy: "sector" 
                      ) {
                        totalResults
                        totalResultsPerPage
                        totalPages
                        currentPage
                        nextPage
                        results {
                          id
                          grant_id 
                          funding_purpose
                          funding_purpose_definition
                          purpose_title
                          purpose_size_max_formatted
                          purpose_description
                          grant {
                            currency
                            provider_1
                          }
                        }
                      }
                }
            ';
            break;
            //Fetch a single Purpose
        case 'fetchPurpose':
            //Get the passed in purpose ID to view otherwise default to 0 in this simple example
            $purposeID = !empty($_GET['id']) ? $_GET['id'] : 0;

            /**
             * Construct the GraphQL query to fetch the purpose profile - you can define exatcly which field you want back to display to the end user
             * 
             * This query will use 1 of our allowed API limits per month the first time it is performed (unless you're on the unlimited package)
             * If this query is performed again (per month), using the exact same purpose ID, then it will be a non-unique query and will not use and of the API limits (only unique queries impact the usage limits)
             * See: https://apidocs.getbusinessgrants.com/#query-purpose
             */
            $query = '
                    query {
                        purpose(id: ' . $purposeID . ') {
                            id
                            grant_id
                            funding_purpose
                            funding_purpose_definition
                            purpose_title
                            purpose_url
                            purpose_application_url
                            purpose_size_min_formatted
                            purpose_size_max_formatted
                            purpose_description
                            purpose_dates
                            purpose_region_is_uk
                            deadline_date
                            date_added
                            grant {
                                id
                                url
                                title
                                open_or_closed
                                currency
                                total_fund_size
                                total_fund_size_formatted
                                provider_1
                                provider_2
                                provider_3
                                provider_4
                                funding_types {
                                    fundingtype
                                }
                                date_added
                            }
                            regions {
                                display_region
                            }
                            sectors {
                                sector
                            }
                            business_types {
                                business_type
                            }
                        }
                    }
                    ';
            break;
    }

    /**
     * Now we have the GraphQL query and we can send it to the Grants API
     * 
     * First we set our headers which includes our API key
     */
    $headers = array(
        'Content-Type: application/json',
        'Authorization: Bearer ' . API_KEY //This is required for all API calls
    );

    //Prepare the data array we'll send to the API
    $data = array(
        'query' => $query,
    );

    /**
     * We'll send the graphQL query using cURL
     */
    $ch = curl_init(API_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    //While in development you might need the below line but not recommended for production
    //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

    // Execute the request
    $response = curl_exec($ch);

    // Check for errors
    if ($response === false) {
        echo 'Error: ' . curl_error($ch);
    }

    // Close cURL
    curl_close($ch);

    /**
     * If we have a response we'll simply send the JSON back to the front end
     * 
     * Note: The Grants API sends back additional headers that will show details of your API subscription depending on your package level. How many unique requests your package allows, how many unique requests you have remaining this month etc etc.
     * 
     * See: https://apidocs.getbusinessgrants.com/#introduction-item-0
     */
    if (!empty($response)) {
        $responseData = json_decode($response, true);
        echo json_encode($responseData['data']);
        exit;
    }
}

/**
 * Here we'll just echo a message rather than do a fully managed JSON response
 */
echo 'Invalid request';