No novels or cover or names are showing or displaying only this error is displaying on the page: No novels found for the given search term. --- 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Light Novel Library</title>

  <!-- Preconnect and Load Fonts Efficiently -->
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">

  <!-- External Libraries -->
  <!-- jQuery -->
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>
  <!-- Dexie for IndexedDB -->
  <script src="https://cdn.jsdelivr.net/npm/dexie@3.2.2/dist/dexie.min.js"></script>
  <!-- Intersection Observer Polyfill for Older Browsers -->
  <script src="https://cdn.jsdelivr.net/npm/intersection-observer@0.12.2/intersection-observer.min.js"></script>
  <!-- AOS Library for Scroll Animations -->
  <link href="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.js"></script>

  <!-- Stylesheet -->
  <style>
    /* Root Variables */
    :root {
      --primary-color: #1e3a8a;
      --secondary-color: #ffffff;
      --background-color: #f0f4f8;
      --card-background: #ffffff;
      --text-primary: #1f2937;
      --text-secondary: #6b7280;
      --shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
      --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
      --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
      --radius-sm: 0.375rem;
      --radius-md: 0.5rem;
      --radius-lg: 1rem;
      --transition-duration: 0.3s;
      --max-width: 1280px;
      --header-height: 80px; /* Define header height for layout adjustments */
    }

    /* Reset and Base Styles */
    *, *::before, *::after {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: 'Inter', sans-serif;
      background-color: var(--background-color);
      color: var(--text-primary);
      line-height: 1.6;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
    }

    /* Main Content Styles */
    .main {
      flex: 1;
      max-width: var(--max-width);
      margin: 2rem auto;
      padding: 0 1rem;
    }

    /* Search and Refresh Styles */
    .search-container {
      margin-bottom: 2rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
    }

    .search-input {
      flex: 1;
      min-width: 250px;
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      border: 2px solid var(--primary-color);
      border-radius: var(--radius-md);
      outline: none;
      transition: border-color var(--transition-duration) ease;
      background-color: var(--secondary-color);
      color: var(--text-primary);
    }

    .search-input:focus {
      border-color: var(--text-secondary);
    }

    .refresh-button {
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      border: none;
      border-radius: var(--radius-md);
      background-color: var(--primary-color);
      color: var(--secondary-color);
      cursor: pointer;
      transition: background-color var(--transition-duration) ease;
      margin-left: 1rem;
      margin-top: 1rem;
    }

    .refresh-button:hover {
      background-color: var(--text-secondary);
    }

    /* Grid Styles */
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 2rem;
    }

    /* Card Styles */
    .card {
      background: var(--card-background);
      border-radius: var(--radius-md);
      overflow: hidden;
      box-shadow: var(--shadow-sm);
      transition: transform var(--transition-duration) ease, box-shadow var(--transition-duration) ease;
      cursor: pointer;
      display: flex;
      flex-direction: column;
      height: 100%;
      opacity: 0; /* For AOS animation */
      animation: fadeInCard 0.5s forwards;
    }

    .card:hover {
      transform: translateY(-5px);
      box-shadow: var(--shadow-lg);
    }

    @keyframes fadeInCard {
      to { opacity: 1; }
    }

    .card__image-container {
      position: relative;
      width: 100%;
      padding-top: 140%; /* 5:7 aspect ratio */
      background: #e0e0e0;
      overflow: hidden;
    }

    .card__image {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      /* Lazy loading for better performance */
      loading: lazy;
    }

    .card__content {
      padding: 1rem;
      flex-grow: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }

    .card__title {
      font-size: 1.1rem;
      font-weight: 600;
      color: var(--text-primary);
      text-align: center;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    /* Loader Styles */
    .loader {
      display: none; /* Initially hidden */
      justify-content: center;
      align-items: center;
      height: 200px;
      color: var(--text-secondary);
    }

    .loader.visible {
      display: flex; /* Show when visible */
    }

    .loader__spinner {
      width: 50px;
      height: 50px;
      border: 5px solid #f3f3f3;
      border-top: 5px solid var(--primary-color);
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }

    /* Error Message Styles */
    .error-message {
      text-align: center;
      color: #ff0000;
      padding: 1rem;
      background: #ffe6e6;
      border-radius: var(--radius-sm);
      margin: 1rem 0;
    }

    /* Modal Styles */
    .modal {
      display: none;
      position: fixed;
      z-index: 2000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      overflow: auto;
      background-color: rgba(0,0,0,0.7);
      justify-content: center;
      align-items: center;
      padding: 1rem;
    }

    .modal-content {
      background-color: var(--secondary-color);
      padding: 2rem;
      border-radius: var(--radius-lg);
      max-width: 700px;
      width: 100%;
      position: relative;
      box-shadow: var(--shadow-lg);
      animation: fadeInModal 0.3s ease;
    }

    @keyframes fadeInModal {
      from { opacity: 0; transform: translateY(-20px); }
      to { opacity: 1; transform: translateY(0); }
    }

    .close {
      position: absolute;
      top: 1rem;
      right: 1.5rem;
      color: var(--text-secondary);
      font-size: 2rem;
      font-weight: bold;
      cursor: pointer;
      transition: color var(--transition-duration) ease;
      background: none;
      border: none;
      font-family: inherit;
    }

    .close:hover,
    .close:focus {
      color: var(--text-primary);
    }

    .modal__title {
      font-size: 1.75rem;
      font-weight: 700;
      margin-bottom: 1rem;
      color: var(--text-primary);
    }

    .modal__link {
      display: inline-block;
      padding: 0.75rem 1.5rem;
      background-color: var(--primary-color);
      color: var(--secondary-color);
      text-decoration: none;
      border-radius: var(--radius-md);
      font-weight: 600;
      transition: background-color var(--transition-duration) ease;
    }

    .modal__link:hover {
      background-color: var(--text-secondary);
    }

    /* Sentinel Element for Infinite Scroll */
    .sentinel {
      height: 1px;
    }

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

    /* Responsive Styles */
    @media (max-width: 768px) {
      .grid {
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 1.5rem;
      }

      .card__title {
        font-size: 1rem;
      }
    }

    @media (max-width: 480px) {
      .grid {
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        gap: 1rem;
      }
    }
  </style>
</head>
<body>
  <!-- Main Content -->
  <main class="main" role="main">
    <!-- Search Bar and Refresh Button -->
    <div class="search-container">
      <input type="text" id="search-input" class="search-input" placeholder="Search light novels..." aria-label="Search light novels">
      <button id="refresh-button" class="refresh-button" aria-label="Refresh Data">Refresh Data</button>
    </div>

    <!-- Content Grid -->
    <div id="content" class="grid" aria-live="polite">
      <!-- Initial Loader -->
      <div class="loader" id="loader" role="status" aria-label="Loading novels">
        <div class="loader__spinner"></div>
      </div>
    </div>

    <!-- Sentinel Element for Infinite Scroll -->
    <div class="sentinel" id="sentinel"></div>
  </main>

  <!-- Modal -->
  <div id="modal" class="modal" aria-hidden="true" role="dialog" aria-labelledby="modal-title">
    <div class="modal-content">
      <button class="close" aria-label="Close modal">&times;</button>
      <h2 id="modal-title" class="modal__title"></h2>
      <a id="modal-link" class="modal__link" href="#" target="_blank" rel="noopener noreferrer">Read PDF</a>
    </div>
  </div>

  <!-- JavaScript -->
  <script>
    'use strict';
    $(document).ready(function() {
      /**
       * Initialize Dexie Database with Unique Constraints on name, coverUrl, and pdfUrl
       */
      const db = new Dexie('MonochromeNovelsDB');
      db.version(30).stores({
        novels: '++id, &name, &coverUrl, &pdfUrl, timestamp' // Unique indexes on name, coverUrl, pdfUrl
      });

      // Constants
      const PAGE_SIZE = 20; // Number of novels to load per page
      let currentPage = 0; // Current page index
      let isLoading = false; // Flag to prevent multiple simultaneous loads
      let hasMore = true; // Flag to determine if more novels are available
      let currentSearchTerm = ''; // Current search term

      const PLACEHOLDER_IMAGE = 'https://via.placeholder.com/500x700?text=No+Image'; // Placeholder for missing images

      /**
       * Debounce Function to Limit Function Execution Rate
       * @param {Function} func
       * @param {number} delay
       * @returns {Function}
       */
      const debounce = (func, delay) => {
        let debounceTimer;
        return function(...args) {
          const context = this;
          clearTimeout(debounceTimer);
          debounceTimer = setTimeout(() => func.apply(context, args), delay);
        }
      };

      /**
       * Utility: Delay Function
       * @param {number} ms
       * @returns {Promise}
       */
      const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

      /**
       * Utility: Retry Operation with Exponential Backoff (3 Attempts)
       * @param {Function} operation
       * @param {number} attempts
       * @param {number} delayDuration
       * @returns {Promise}
       */
      const retryOperation = async (operation, attempts = 3, delayDuration = 1500) => {
        for (let i = 0; i < attempts; i++) {
          try {
            return await operation();
          } catch (error) {
            if (i === attempts - 1) throw error;
            await delay(delayDuration * Math.pow(2, i)); // Exponential backoff
          }
        }
      };

      /**
       * Initialize Intersection Observer for Infinite Scroll using Sentinel Element
       */
      const setupSentinelObserver = () => {
        const sentinel = $('#sentinel')[0];
        if (!sentinel) return;

        const sentinelObserver = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting && hasMore && !isLoading) {
              loadNextPage();
            }
          });
        }, {
          rootMargin: '100px',
          threshold: 0
        });
        sentinelObserver.observe(sentinel);
      };

      /**
       * Load Next Page of Novels from IndexedDB
       */
      const loadNextPage = async () => {
        if (isLoading) return; // Prevent multiple simultaneous loads
        isLoading = true;
        showLoader();

        try {
          let novels;

          if (currentSearchTerm === '') {
            novels = await db.novels
              .orderBy('name') // Order by name for alphabetical display
              .offset(currentPage * PAGE_SIZE)
              .limit(PAGE_SIZE)
              .toArray();
          } else {
            novels = await db.novels
              .where('name')
              .startsWithIgnoreCase(currentSearchTerm)
              .offset(currentPage * PAGE_SIZE)
              .limit(PAGE_SIZE)
              .toArray();
          }

          if (novels.length < PAGE_SIZE) {
            hasMore = false; // No more novels to load
          }

          if (novels.length > 0) {
            appendNovelsToDisplay(novels);
            currentPage++;
          } else {
            if (currentPage === 0) {
              showError('No novels found for the given search term.');
            }
          }
        } catch (error) {
          console.error('Error loading novels:', error);
          showError('An error occurred while loading novels.');
        } finally {
          hideLoader();
          isLoading = false;
        }
      };

      /**
       * Show Loader
       */
      const showLoader = () => {
        $('#loader').addClass('visible');
      };

      /**
       * Hide Loader
       */
      const hideLoader = () => {
        $('#loader').removeClass('visible');
      };

      /**
       * Display Error Message
       * @param {string} message
       */
      const showError = message => {
        const $content = $('#content');
        // Remove existing error messages
        $content.find('.error-message').remove();

        const $errorDiv = $('<div>', {
          class: 'error-message',
          role: 'alert',
          text: message
        });
        $content.append($errorDiv);
        hideLoader();
      };

      /**
       * Append Novels to Display
       * @param {Array} novels
       */
      const appendNovelsToDisplay = novels => {
        const $content = $('#content');
        const $fragment = $(document.createDocumentFragment());

        novels.forEach(novel => {
          const $card = createNovelCard(novel);
          $fragment.append($card);
        });

        $content.append($fragment);
        AOS.refresh(); // Refresh AOS to apply animations to new elements
      };

      /**
       * Create Novel Card Element
       * @param {Object} novel
       * @returns {jQuery}
       */
      const createNovelCard = (novel) => {
        const $card = $('<div>', {
          class: 'card',
          tabindex: 0,
          role: 'button',
          'aria-labelledby': `novel-title-${encodeURIComponent(novel.name)}`,
          'data-aos': 'fade-up'
        });

        // Construct HTML Structure: Image on top, title below
        const $imageContainer = $('<div>', { class: 'card__image-container' });
        const $image = $('<img>', {
          class: 'card__image',
          src: escapeHTML(novel.coverUrl) || PLACEHOLDER_IMAGE,
          alt: `Cover of ${escapeHTML(novel.name)}`,
          error: function() {
            $(this).attr('src', PLACEHOLDER_IMAGE);
          },
          loading: 'lazy'
        });
        $imageContainer.append($image);

        const $content = $('<div>', { class: 'card__content' });
        const $title = $('<h2>', {
          id: `novel-title-${encodeURIComponent(novel.name)}`,
          class: 'card__title',
          text: escapeHTML(novel.name)
        });
        $content.append($title);

        $card.append($imageContainer, $content);

        // Event Listener to Open Modal
        $card.on('click', () => {
          showModal(novel);
        });

        // Keyboard Accessibility: Open Modal on Enter or Space Key
        $card.on('keydown', event => {
          if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            showModal(novel);
          }
        });

        return $card;
      };

      /**
       * Utility: Escape HTML to Prevent XSS
       * @param {string} str
       * @returns {string}
       */
      const escapeHTML = (str) => {
        if (!str) return '';
        return $('<div>').text(str).html();
      };

      /**
       * Modal Functionality
       */
      const $modal = $('#modal');
      const $modalTitle = $('#modal-title');
      const $modalLink = $('#modal-link');
      const $closeBtn = $('.close');

      /**
       * Show Modal with Novel Details
       * @param {Object} novel
       */
      const showModal = (novel) => {
        $modalTitle.text(novel.name);
        $modalLink.attr('href', novel.pdfUrl);
        $modal.fadeIn(200).css('display', 'flex').attr('aria-hidden', 'false');
        $('body').css('overflow', 'hidden'); // Prevent background scrolling
        $closeBtn.focus(); // Focus on close button for accessibility
      };

      /**
       * Close Modal
       */
      const closeModal = () => {
        $modal.fadeOut(200).attr('aria-hidden', 'true');
        $('body').css('overflow', 'auto');
      };

      $closeBtn.on('click', closeModal);

      $(window).on('click', event => {
        if ($(event.target).is($modal)) {
          closeModal();
        }
      });

      $(window).on('keydown', event => {
        if (event.key === 'Escape' && $modal.is(':visible')) {
          closeModal();
        }
      });

      /**
       * Search Functionality
       */
      const $searchInput = $('#search-input');

      const handleSearch = debounce(async () => {
        currentSearchTerm = $searchInput.val().trim();
        currentPage = 0;
        hasMore = true;

        const $content = $('#content');
        $content.empty(); // Clear existing content
        $content.find('.error-message').remove();

        // Reload the first page with the new search term
        await loadNextPage();
      }, 300); // 300ms debounce delay

      $searchInput.on('input', handleSearch);

      /**
       * Refresh Data Functionality
       */
      const $refreshButton = $('#refresh-button');
      $refreshButton.on('click', async () => {
        const confirmation = confirm('Are you sure you want to refresh the data? This may take a few moments.');
        if (!confirmation) return;

        isLoading = true;
        showLoader();
        hasMore = true;
        currentPage = 0;

        try {
          // Clear existing data
          await db.novels.clear();

          // Fetch and parse fresh data
          await fetchAndStoreNovels();

          // Reload the first page of novels
          const $content = $('#content');
          $content.empty(); // Clear existing content
          $content.find('.error-message').remove();

          await loadNextPage();
        } catch (error) {
          console.error('Error refreshing data:', error);
          showError('Failed to refresh data. Please try again later.');
        } finally {
          hideLoader();
          isLoading = false;
        }
      });

      /**
       * Initialize Application
       */
      const initializeApp = async () => {
        try {
          // Check if any novels are already stored in IndexedDB
          const count = await db.novels.count();
          if (count === 0) {
            // If not, fetch from the target website
            await fetchAndStoreNovels();
          }

          // Load the first page of novels
          await loadNextPage();
        } catch (error) {
          console.error('Error initializing application:', error);
          showError('Failed to load novels. Please try refreshing the page.');
        }
      };

      /**
       * Fetch and Store Novels
       */
      const fetchAndStoreNovels = async () => {
        const html = await retryOperation(() => fetchHTML('https://jnovels.com/top-light-novels-to-read/'));
        let novels = parseHTML(html);

        if (novels.length === 0) {
          throw new Error('No novels found on the target page.');
        }

        // Bulk upsert novels to IndexedDB
        try {
          await db.novels.bulkPut(novels);
        } catch (bulkPutError) {
          if (bulkPutError.name === 'BulkError') {
            // Ignore duplicate entries and continue
            console.warn('Some novels were duplicates and were skipped.');
          } else {
            console.error('Error bulk upserting novels:', bulkPutError);
            throw bulkPutError; // Re-throw to be handled by caller
          }
        }
      };

      /**
       * Fetch HTML Content with Content-Type Validation via CORS Proxy
       * @param {string} url
       * @returns {Promise<string>}
       */
      const fetchHTML = async (url) => {
        // Using allorigins API to bypass CORS
        const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
        const response = await $.ajax({
          url: proxyUrl,
          method: 'GET',
          dataType: 'json',
          timeout: 10000 // 10 seconds timeout
        });

        if (!response || !response.contents) {
          throw new Error('Failed to fetch content from proxy.');
        }

        // Check Content-Type Header (allorigins returns in the contents)
        // Since allorigins returns the content directly, we assume it's HTML
        return response.contents;
      };

      /**
       * Parse Fetched HTML to Extract Novel Data Based on Provided Schema
       * @param {string} html
       * @returns {Array<Object>}
       */
      const parseHTML = (html) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const novels = [];
        const uniqueNovels = new Set();

        // XPath selectors
        const xpathBookName = '//h3/a/span[@style="color: #ff6600;"]';
        const xpathPdfLink = '//h3/a/@href';
        const xpathCoverUrl = '//h3/following-sibling::p[1]/a/img/@src';

        const bookNameNodes = doc.evaluate(xpathBookName, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        const pdfLinkNodes = doc.evaluate(xpathPdfLink, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        const coverUrlNodes = doc.evaluate(xpathCoverUrl, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        for (let i = 0; i < bookNameNodes.snapshotLength; i++) {
          try {
            const nameNode = bookNameNodes.snapshotItem(i);
            const pdfLinkNode = pdfLinkNodes.snapshotItem(i);
            const coverUrlNode = coverUrlNodes.snapshotItem(i);

            const name = nameNode ? nameNode.textContent.trim() : '';
            const pdfLink = pdfLinkNode ? pdfLinkNode.nodeValue.trim() : '';
            let coverUrl = coverUrlNode ? coverUrlNode.nodeValue.trim() : '';

            // Handle different URL formats
            if (coverUrl) {
              if (coverUrl.startsWith('//')) {
                coverUrl = `https:${coverUrl}`;
              } else if (coverUrl.startsWith('/')) {
                coverUrl = `https://jnovels.com${coverUrl}`;
              } else if (!coverUrl.startsWith('http')) {
                // Handle relative URLs without leading '/'
                coverUrl = `https://jnovels.com/${coverUrl}`;
              }
            }

            // Validate extracted data
            if (name && pdfLink && coverUrl) {
              const novelKey = `${name}-${coverUrl}-${pdfLink}`;
              if (!uniqueNovels.has(novelKey)) {
                uniqueNovels.add(novelKey);
                novels.push({
                  name,
                  coverUrl,
                  pdfUrl: pdfLink,
                  timestamp: Date.now()
                });
              }
            } else {
              console.warn('Incomplete novel data found and skipped:', { name, pdfLink, coverUrl });
            }
          } catch (error) {
            // Skip any malformed entries
            console.error('Error parsing a novel entry:', error);
          }
        }

        return novels;
      };

      /**
       * Initialize AOS and Start the Application once the DOM is fully loaded
       */
      AOS.init({
        duration: 800,
        easing: 'ease-in-out',
        once: true,
      });

      setupSentinelObserver();
      initializeApp();
    });
  </script>
</body>
</html>