The Search Feature

The Search page embeds the Search App Scripts app.  The search feature displays a search box and search button that stays static at the top of the page.  When the user enters any search text it escaped into text characters only so that potential hacking tricks cannot get performed.   A favorite trick of hackers is to use the input boxes to send executable code to the server.  If it is text characters only then it will not get executed as code.

When the user hits the search button the API that handles searching will be called.  This API call will accept the search text, find any matches and then return a Json object that contains the search results.  The search results display under the top search bar.  The Json object are fields the search results display will fill in and display to the user.

The frontend code will accept the Json and loop through the object to display it into the Search Card Interface.  This is done for up to 50 records.  We are only getting the first 50 records because most will find what they seek within that first 50 records or try a different search.  If we pull 100+ records, it will be a waste of server resources in almost all searches.

If the user refreshes a search the page will reload the current Json object's data.  A new call to the API will not occur, that only occur when the search button is clicked.

App Scripts code engine is used.  The code between the "<?  ?>" represents executable App Scripts engine code.  This code will only execute on the App Scripts servers.  This is why we embed the app into the page instead of copying the code into the page.


In the <head> tag is where we'd place local JavaScript for the Search feature.  This is frontend code.  We do some validation and call the Code file to get the search results.  We place custom CSS code that the loaded CSS library, W3.CSS, doesn't handle.


 The frontend code is below.   Note the versions of libraries.  This affects what can be done.  This is a temp layout and API.


Code View

Code.gs
// Code.gs
/**
 * Serves the initial HTML page (Index.html).
 * @returns {GoogleAppsScript.HTML.HtmlOutput} The HTML output for the web app.
 */
function doGet() {
  // Create a template from the Index.html file.
  const template = HtmlService.createTemplateFromFile('Index');
  // Evaluate the template and set the title.
  return template.evaluate()
      .setTitle('W3.CSS Search App');
}

/**
 * Includes an HTML file as a template.
 * This function is used within HTML templates (e.g., Index.html) to include other HTML files.
 * @param {string} filename The name of the HTML file to include (without .html extension).
 * @returns {string} The content of the included HTML file.
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

/**
 * Performs a search using the Open Library API and returns structured results.
 * Implements basic server-side validation and error handling.
 * @param {string} searchText The text to search for.
 * @returns {Object} An object containing search results or an error message.
 */
function performSearch(searchText) {
  // Basic server-side validation to prevent obvious XSS and empty queries.
  if (!searchText || typeof searchText !== 'string' || searchText.trim() === '') {
    return { error: 'Search text cannot be empty.' };
  }

  // Sanitize input: remove potential script tags or event handlers.
  // Although HtmlService.HtmlOutput.evaluate() handles output escaping,
  // it's good practice to validate input before using it in a URL fetch.
  const sanitizedSearchText = searchText.replace(/<script.*?>.*?<\/script>/gi, '')
                                        .replace(/on\w+=".*?"/gi, '')
                                        .trim();

  if (sanitizedSearchText === '') {
    return { error: 'Invalid search text after sanitization.' };
  }

  // Encode the search text for URL.
  const encodedSearchText = encodeURIComponent(sanitizedSearchText);
  const apiUrl = `https://openlibrary.org/search.json?q=${encodedSearchText}&limit=50`; // Limit to 50 results as requested.

  try {
    // Fetch data from the Open Library API.
    const response = UrlFetchApp.fetch(apiUrl);
    const jsonResponse = JSON.parse(response.getContentText());

    // Process the results.
    const results = [];
    if (jsonResponse && jsonResponse.docs) {
      jsonResponse.docs.forEach(doc => {
        results.push({
          title: doc.title || 'No Title',
          author: doc.author_name ? doc.author_name.join(', ') : 'Unknown Author',
          firstPublishYear: doc.first_publish_year || 'N/A',
          // Construct a cover image URL if available
          coverUrl: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : 'https://placehold.co/128x192/cccccc/333333?text=No+Cover'
        });
      });
    }
    return { success: true, results: results };

  } catch (e) {
    // Log the error for debugging.
    console.error('Error fetching from Open Library API:', e.message);
    return { error: 'Failed to fetch search results. Please try again later.' };
  }
}
 

Index.html

<!-- Index.html -->
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <title>W3.CSS Search App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://www.w3schools.com/w3css/5/w3.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <style>
    body, h1, h2, h3, h4, h5, h6 {font-family: "Inter", sans-serif;}
    .w3-card { border-radius: 8px; }
    .w3-button { border-radius: 8px; }
    .w3-input { border-radius: 8px; }
    .w3-container { padding: 16px; }
    .w3-center { text-align: center; }
    .w3-padding-large { padding: 24px !important; }
    .w3-margin-top { margin-top: 16px !important; }
    .w3-margin-bottom { margin-bottom: 16px !important; }
    .w3-display-middle {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      -ms-transform: translate(-50%, -50%);
    }
    .loading-spinner {
      border: 4px solid #f3f3f3; /* Light grey */
      border-top: 4px solid #3498db; /* Blue */
      border-radius: 50%;
      width: 30px;
      height: 30px;
      animation: spin 1s linear infinite;
      margin: 20px auto;
      display: none; /* Hidden by default */
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    /* Responsive grid for results */
    .w3-row-padding .w3-col {
      padding: 8px;
    }
    @media (min-width: 601px) {
      .w3-half { width: 50%; }
      .w3-third { width: 33.33%; }
      .w3-quarter { width: 25%; }
    }
  </style>
</head>
<body class="w3-light-grey">

  <!-- Header -->
  <div class="w3-bar w3-blue-grey w3-card-4 w3-round-large w3-padding-small">
    <a href="#" class="w3-bar-item w3-button w3-round-large w3-hover-light-grey" onclick="loadHomePage()">Home</a>
    <span class="w3-bar-item w3-right w3-large">Public Search App</span>
  </div>

  <!-- Main content area where pages will be loaded -->
  <div id="content-area" class="w3-container w3-padding-large w3-margin-top">
    <?!= include('Home'); ?>
  </div>

  <!-- Loading Spinner -->
  <div id="loadingSpinner" class="loading-spinner"></div>

  <script>
    // Client-side JavaScript for handling page transitions and search.

    /**
     * Shows or hides the loading spinner.
     * @param {boolean} show True to show, false to hide.
     */
    function toggleLoadingSpinner(show) {
      document.getElementById('loadingSpinner').style.display = show ? 'block' : 'none';
    }

    /**
     * Loads the Home page content into the main content area.
     */
    function loadHomePage() {
      toggleLoadingSpinner(true);
      google.script.run
        .withSuccessHandler(function(html) {
          document.getElementById('content-area').innerHTML = html;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          loadErrorPage(error.message); // Load error page on failure
          toggleLoadingSpinner(false);
        })
        .include('Home'); // Call the server-side include function
    }

    /**
     * Performs a search when the search button is clicked.
     */
    function performSearch() {
      const searchInput = document.getElementById('searchInput');
      let searchText = searchInput.value;

      // Client-side XSS validation: Basic check for script tags and event handlers.
      // This is a first line of defense; server-side validation is more critical.
      const scriptRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
      const eventHandlerRegex = /on\w+="[^"]*"/gi;

      if (scriptRegex.test(searchText) || eventHandlerRegex.test(searchText)) {
        loadErrorPage('Invalid characters detected. Please remove script tags or event handlers.');
        return;
      }

      if (searchText.trim() === '') {
        loadErrorPage('Please enter some text to search.');
        return;
      }

      toggleLoadingSpinner(true); // Show spinner

      // Call the server-side function to perform the search.
      google.script.run
        .withSuccessHandler(function(response) {
          toggleLoadingSpinner(false); // Hide spinner
          if (response.success) {
            // Load the Results.html template with the fetched data.
            loadResultsPage(response.results);
          } else {
            loadErrorPage(response.error || 'An unknown error occurred during search.');
          }
        })
        .withFailureHandler(function(error) {
          toggleLoadingSpinner(false); // Hide spinner
          loadErrorPage('Error during search: ' + error.message);
        })
        .performSearch(searchText);
    }

    /**
     * Loads the Results page content into the main content area with search results.
     * @param {Array} results An array of search result objects.
     */
    function loadResultsPage(results) {
      toggleLoadingSpinner(true);
      // Call the server-side include function for Results.html.
      // Pass the results data to the template.
      google.script.run
        .withSuccessHandler(function(html) {
          // Replace placeholders in the HTML with actual data.
          let populatedHtml = html;
          if (results && results.length > 0) {
            let resultsHtml = '';
            results.forEach(item => {
              resultsHtml += `
                <div class="w3-col l3 m4 s6 w3-margin-bottom">
                  <div class="w3-card w3-white w3-round-large w3-hover-shadow">
                    <img src="${item.coverUrl}" alt="Book Cover" class="w3-image w3-round-t-large" style="width:100%; height: 192px; object-fit: cover;" onerror="this.onerror=null;this.src='[https://placehold.co/128x192/cccccc/333333?text=No+Cover](https://placehold.co/128x192/cccccc/333333?text=No+Cover)';">
                    <div class="w3-container w3-padding-small">
                      <h4 class="w3-text-blue-grey w3-small"><b>${item.title}</b></h4>
                      <p class="w3-tiny">Author: ${item.author}</p>
                      <p class="w3-tiny">Published: ${item.firstPublishYear}</p>
                    </div>
                  </div>
                </div>
              `;
            });
            populatedHtml = populatedHtml.replace('<!-- SEARCH_RESULTS_PLACEHOLDER -->', resultsHtml);
          } else {
            populatedHtml = populatedHtml.replace('<!-- SEARCH_RESULTS_PLACEHOLDER -->', '<p class="w3-center w3-text-grey">No results found for your search.</p>');
          }
          document.getElementById('content-area').innerHTML = populatedHtml;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          loadErrorPage('Failed to load results page: ' + error.message); // Load error page on failure
          toggleLoadingSpinner(false);
        })
        .include('Results'); // Call the server-side include function
    }

    /**
     * Loads the custom error page with a given message.
     * @param {string} errorMessage The message to display on the error page.
     */
    function loadErrorPage(errorMessage) {
      toggleLoadingSpinner(true);
      google.script.run
        .withSuccessHandler(function(html) {
          // Replace the placeholder in the error HTML with the actual message.
          const populatedHtml = html.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->', errorMessage);
          document.getElementById('content-area').innerHTML = populatedHtml;
          toggleLoadingSpinner(false);
        })
        .withFailureHandler(function(error) {
          // Fallback if even the error page fails to load
          document.getElementById('content-area').innerHTML = `
            <div class="w3-panel w3-red w3-round-large w3-padding-large w3-margin-top w3-center">
              <h3>Critical Error!</h3>
              <p>Could not load error page. Original error: ${errorMessage}</p>
              <p>Further error: ${error.message}</p>
              <button class="w3-button w3-white w3-round-large w3-margin-top" onclick="loadHomePage()">Go to Home</button>
            </div>
          `;
          toggleLoadingSpinner(false);
        })
        .include('Error'); // Call the server-side include function for Error.html
    }
  </script>
</body>
</html>


Home.html
<!-- Home.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top w3-center">
  <h2 class="w3-text-blue-grey">Welcome to the Public Search App!</h2>
  <p>Enter your search query below to find books from Open Library.</p>

  <div class="w3-row w3-section w3-center">
    <div class="w3-col" style="width:50px"><i class="w3-xxlarge fa fa-search"></i></div>
    <div class="w3-rest">
      <input class="w3-input w3-border w3-round-large" name="search" type="text" placeholder="Search for books..." id="searchInput" onkeydown="if(event.keyCode === 13) performSearch()">
    </div>
  </div>

  <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large w3-padding-large" onclick="performSearch()">
    <i class="fa fa-search"></i> Search
  </button>

  <div class="w3-panel w3-light-grey w3-round-large w3-padding-large w3-margin-top w3-border w3-border-blue-grey">
    <h3 class="w3-text-blue-grey">Ad Space</h3>
    <p>Your advertisement could go here! This space is available for promotions.</p>
    <img src="https://placehold.co/300x100/4CAF50/ffffff?text=Your+Ad+Here" alt="Advertisement" class="w3-image w3-round-large" style="max-width:100%;">
    <p class="w3-small w3-text-grey">Contact us for ad placement.</p>
  </div>
</div>
 


Results.html

<!-- Results.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top">
  <h2 class="w3-text-blue-grey w3-center">Search Results</h2>
  <p class="w3-center w3-text-grey">Displaying up to 50 results from Open Library.</p>

  <div class="w3-row-padding w3-margin-top">
    <!-- SEARCH_RESULTS_PLACEHOLDER -->
    <!-- This placeholder will be replaced by JavaScript with the actual search results -->
  </div>

  <div class="w3-center w3-margin-top">
    <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large" onclick="loadHomePage()">Back to Home</button>
  </div>
</div>





Error.html

<!-- Results.html -->
<div class="w3-container w3-card-4 w3-white w3-round-large w3-padding-large w3-margin-top">
  <h2 class="w3-text-blue-grey w3-center">Search Results</h2>
  <p class="w3-center w3-text-grey">Displaying up to 50 results from Open Library.</p>

  <div class="w3-row-padding w3-margin-top">
    <!-- SEARCH_RESULTS_PLACEHOLDER -->
    <!-- This placeholder will be replaced by JavaScript with the actual search results -->
  </div>

  <div class="w3-center w3-margin-top">
    <button class="w3-button w3-blue-grey w3-hover-light-grey w3-round-large" onclick="loadHomePage()">Back to Home</button>
  </div>
</div>



The Gemini text to get the code with only a few moderations I had to make as noted:


Create a multiple page search app using w3.css and google app script. Use app scripts multi page html templating features.  Load a Home html template page as default, allow ad space on it using W3.CSS. The Index page should have a search bar for users to enter the search text and click a search button to load the Results html template page using W3.CSS. Validate the text input to avoid cross script hacking.

Create code in the Code.gs that uses the text, validates it and send it to the search API to et search results.  When the user hits the search button load the Results html template page interface into the index page. Use a public API to test display results up to 50 records from the public API in a grid display. The interface should be a card using w3.css. The Home page is the default load. The Results page load into the Index when the search button is clicked.

Important Considerations:

Public API Key Security: If your chosen public API requires an API key, do not expose it directly in client-side JavaScript. Instead, store it securely in your Code.gs file and make all API calls from the Google Apps Script backend (using UrlFetchApp). This way, your API key is never visible to the end-user.

Error Handling: Implement more robust error handling on both the client and server sides.

Pagination/Load More: For APIs returning more than 50 results, you'll need to implement pagination (e.g., "next page" button) or infinite scrolling. This would involve passing offset or page parameters to your performSearch function.

Rate Limiting: Be mindful of rate limits imposed by the public API you choose.

Real XSS Prevention: The provided client-side XSS validation is basic. For production applications, consider using a dedicated JavaScript sanitization library (like DOMPurify if you're working with DOM manipulation directly) or rely heavily on server-side output encoding (which Apps Script often handles well when using HtmlService).

User Experience: Add loading spinners, empty state messages, and clearer error messages.

Search Filters/Sorting: Extend the search interface to include options for filtering or sorting results.

Favicon: Add a favicon to your HTML.

Responsive Design: W3.CSS is inherently responsive, but always test on different screen sizes.

CSS Customization: Further customize the W3.CSS styles to match your brand.

Can you add error checking and custom Error html template page that loads on error with error message. Use W3.CSS to style it.





Status Update: Corrected Websites, Again.


The test and production versions of the app can be found at the Google Site's website for MyProfiles at:

https://myprofiles.aicodeprojects.com/


I broke the app up into two apps that will embed into pages at the Google Sites website for the MyProfiles website and app, one for Search and one for Admin.
All of the test versions can be found under the TestApp menu.

The production version (not fully completed yet) will be at the same site under the Search menu item to perform profile searches and under the Admin menu item to perform Account Admin, Profile Search Card Admin or View reports.  


The Search menu item will load the Profile Search mini app.  Users will not have to log in to use the search feature.  A search box at the top and the results when requested and found underneath.


The Admin menu will load the Admin mini app.  The user will have to log in if s/he isn't already logged in.  The Admin mini app has a sidebar to jump to features that will load to the right of the sidebar.  The user will be able to view and update account information, view and modify their profile search card data and view stats for their profile search card.


This should help speed things up and have them easier to find and play with.


Enjoy.

Status Update: Corrected Websites.


When lessons are posted I needed a proper place to display them.  

I was using the Google Sites website for testing.  That should be for the final completely version of all lessons only.  The lessons should have their own test site.

So, I've created a test and production version of the lessons.


The test site pages from lessons ae at:

 https://aicodeprojects.w3spaces.com/.


The production version (not fully completed yet) is at:

  https://myprofiles.aicodeprojects.com/.

MyProfiles Lesson 8 - MyProfiles AccountAdmin HTML File

 

This lesson will guide you through creating the Account Admin HTML template. When the user logs into the web app, they are redirected to this file.  The API backend using Firebase Functions won't be covered here.



The Account Admin Feature
  
The Account admin feature is where the user will land once logged into the webapp.  The following functions are available:


1.  View Current Default Login Information

Here the user can see the current default social login account being used for login MyProfiles.  

An Avatar and the email that receives email from MyProfiles is viewable as read-only.


2. Attach More Social Accounts to the MyProfiles Account.


Here will display a list of Social Accounts to attach or detach to the MyProfiles Account.  

Attached Social Accounts allow for faster login.  Any attached account can get set to be the default account that will receive email form MyProfiles.  

The Avatar is private to the MyProfiles account and doesn't appear in the public search engine.  It is recommended that the login and the Social Profiles Search account are different accounts. This can help to protect your MyProfiles account since it'll make it more difficult for potential hackers to guess your login information.

The list will show the attach or detach status of that social account.  The top 5 social networks for US citizens are in the list for users to choose to attach or detach.

If there are 2 or more attached social accounts the user can set anyone to the default.  The email of the default social account will receive MyProfiles emails.



3.  DELETE MyProfiles Account

Here the user can go through the process of deleting their MyProfiles.  

A deletion will delete all account information, and the emails will no longer receive MyProfiles email.  The version of the Profile Search Card information at deletion will be the data that is archived.  This means the data can still be found but it cannot be edited.  If the links on the card produces errors, then the card data will delete.  This is done because it will mean the card data no longer goes to an active social network profile.  If the link goes to an active account, then it will remain accessible.  This allows visitors to find any old accounts they have but never deleted at the social network.

 



Account Admin Layout 

The Account Admin is mostly read only.  There are two panels reading left to right; left and right.

The left panel contains the Account data, Avatar, Account name, Account email and a DELETE button.
The DELETE button will pop up a Delete process form.

The right panel contains Social Networks.  The user can attach or detach a social network as well as set one for default.  This is for logging into the account only.  The default account will contain the email where MyProfiles emails will be sent.


Code View


 
<!DOCTYPE html>
<html>
<title>W3.CSS</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/5/w3.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />

<body>


NOTE: All of the code lines above have to be in the Index.html file that all subpages will load into.



<h1>Account Admin Test Page</h1>


 <div id="content">    

  </div>

      <div class="w3-grid"
           style="grid-template-columns:auto auto auto auto;column-gap:8px;row-gap:12px">



  <div class="w3-container w3-center w3-theme">

 <!-- The Grid -->
  <div class="w3-row-padding">
 
    <!-- Left Column -->
    <div class="w3-third">
   
      <div class="w3-white w3-text-grey w3-card-4">

        <div class="w3-display-container">
          <img src="https://placehold.co/400x200" style="width:100%" alt="Avatar" />
        </div>
       
        <div class="w3-container">
          <h2>Jane Doe</h2>
          <p><i class="fa fa-envelope fa-fw w3-margin-right w3-large w3-text-teal"></i>ex@mail.com</p>
          <hr>
          <button class="w3-button w3-red w3-block w3-margin-bottom">DELETE</button>
    </div>

</div>

    <!-- End Left Column -->
    </div>

    <!-- Right Column -->
   

          <!-- Right Column -->
          <div class="w3-twothird">

            <div class="w3-container w3-light-grey w3-text-blue  w3-card-4 w3-responsive">
              <form action="" class=" w3-section w3-margin">

                <table class="w3-table-all w3-centered">
                  <tr class="w3-theme-d1">
                    <th>Social Account</th>
                    <th>Is Attached?</th>
                    <th>Make Default?</th>
                  </tr>
                  <tr>
                    <td class="w3-left-align"><i class="w3-xxlarge fa fa-user fa-fw"></i>Google</td>
                    <td>
                      <input id="googleisattached" class="w3-check" type="checkbox" checked="checked">
                      <label for="googleisattached">Is Attached</label>

                    </td>
                    <td>

                      <input class="w3-radio" type="radio" id="googleisdefault" name="fav_language" value="Is Default" checked>
                      <label for="googleisdefault">Is Default</label>

                    </td>
                  </tr>
                  <tr>
                    <td class="w3-left-align"><i class="w3-xxlarge fa fa-user fa-fw"></i>Microsoft</td>
                    <td>

                      <input id="microsoftisattached" class="w3-check" type="checkbox">
                      <label for="microsoftisattached">Is Attached</label>
                    </td>
                    <td>


                      <input class="w3-radio" type="radio" id="microsoftisdefault" name="fav_language" value="Is Default">
                      <label for="microsoftisdefault">Is Default</label>

                    </td>
                  </tr>
                  <tr>
                    <td class="w3-left-align"><i class="w3-xxlarge fa fa-user fa-fw"></i>Facebook</td>
                    <td>

                      <input id="facebookisattached" class="w3-check" type="checkbox">
                      <label for="facebookisattached">Is Attached</label>

                    </td>
                    <td>

                      <input class="w3-radio" type="radio" id="facebookisdefault" name="fav_language" value="Is Default">
                      <label for="facebookisdefault">Is Default</label>

                    </td>
                  </tr>
                  <tr>
                    <td class="w3-left-align"><i class="w3-xxlarge fa fa-user fa-fw"></i>Instagram</td>
                    <td>

                      <input id="instagramisattached" class="w3-check" type="checkbox">
                      <label for="instagramisattached">Is Attached</label>

                    </td>
                    <td>

                      <input class="w3-radio" type="radio" id="instagramisdefault" name="fav_language" value="Is Default">
                      <label for="instagramisdefault">Is Default</label>
                    </td>
                  </tr>
                  <tr>
                    <td class="w3-left-align"><i class="w3-xxlarge fa fa-user fa-fw"></i>TikTok</td>
                    <td>

                      <input id="tiktokisattached" class="w3-check" type="checkbox">
                      <label for="tiktokisattached">Is Attached</label>

                    </td>
                    <td>

                      <input class="w3-radio" type="radio" id="tiktokisdefault" name="fav_language" value="Is Default">
                      <label for="tiktokisdefault">Is Default</label>
                    </td>
                  </tr>
                </table>

                <button class="w3-button w3-block w3-section w3-blue w3-ripple w3-padding">Save</button>

              </form>
            </div>

            <!-- End Right Column -->
          </div
   

  <!-- End Grid -->
  </div>
 </div>


 </div>