<!-- 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>