Goondex/docs/HTML_TEMPLATES_GUIDE.md
Stu Leak 16fb407a3c v0.1.0-dev4: Add web frontend with UI component library
- Implement full web interface with Go html/template server
- Add GX component library (buttons, dialogs, tables, forms, etc.)
- Create scene/performer/studio/movie detail and listing pages
- Add Adult Empire scraper for additional metadata sources
- Implement movie support with database schema
- Add import and sync services for data management
- Include comprehensive API and frontend documentation
- Add custom color scheme and responsive layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 10:47:30 -05:00

30 KiB

Goondex HTML/CSS Templates Guide

For Frontend Developers - HTML/CSS Focus

This guide provides complete, ready-to-use HTML templates with Bootstrap styling. All JavaScript is included as simple copy-paste snippets with detailed comments.

Getting Started

All you need to know:

  1. Copy the HTML template you need
  2. The JavaScript is already included in <script> tags - no modifications needed
  3. Customize the CSS/Bootstrap classes for styling
  4. Use Bootstrap 5 components for quick UI elements

Table of Contents

  1. Complete Search Page
  2. Performer Import Form
  3. Bulk Import with Progress
  4. Simple Data Display
  5. Bootstrap Component Reference

Complete Search Page

File: search.html

Copy this entire file - it's ready to use!

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

  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

  <!-- Custom CSS -->
  <style>
    body {
      background-color: #f8f9fa;
    }

    .search-container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 2rem;
    }

    .performer-card {
      transition: transform 0.2s;
      height: 100%;
    }

    .performer-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    }

    .performer-image {
      height: 300px;
      object-fit: cover;
    }

    .no-results {
      text-align: center;
      padding: 3rem;
      color: #6c757d;
    }
  </style>
</head>
<body>

  <!-- Navigation -->
  <nav class="navbar navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Goondex</a>
    </div>
  </nav>

  <!-- Main Content -->
  <div class="search-container">
    <h1 class="mb-4">Search</h1>

    <!-- Search Form -->
    <form id="search-form" class="mb-5">
      <div class="input-group input-group-lg">
        <input
          type="text"
          id="search-input"
          class="form-control"
          placeholder="Search performers, studios, scenes..."
          required
        >
        <button type="submit" class="btn btn-primary px-5">
          Search
        </button>
      </div>
    </form>

    <!-- Loading Spinner (hidden by default) -->
    <div id="loading" class="text-center" style="display: none;">
      <div class="spinner-border text-primary" role="status">
        <span class="visually-hidden">Loading...</span>
      </div>
      <p class="mt-2">Searching...</p>
    </div>

    <!-- Results Container -->
    <div id="results"></div>
  </div>

  <!-- JavaScript -->
  <script>
    // Configuration - change this if your server runs on a different port
    const API_BASE = 'http://localhost:8080';

    // Get elements
    const form = document.getElementById('search-form');
    const input = document.getElementById('search-input');
    const loading = document.getElementById('loading');
    const results = document.getElementById('results');

    // When form is submitted
    form.addEventListener('submit', async function(event) {
      event.preventDefault(); // Don't reload the page

      const searchTerm = input.value.trim();
      if (!searchTerm) return;

      // Show loading, hide results
      loading.style.display = 'block';
      results.innerHTML = '';

      try {
        // Make API request
        const response = await fetch(
          `${API_BASE}/api/search?q=${encodeURIComponent(searchTerm)}`
        );
        const data = await response.json();

        // Hide loading
        loading.style.display = 'none';

        // Check if successful
        if (!data.success) {
          results.innerHTML = `
            <div class="alert alert-danger">
              ${data.message}
            </div>
          `;
          return;
        }

        // Display results
        displayResults(data.data);

      } catch (error) {
        loading.style.display = 'none';
        results.innerHTML = `
          <div class="alert alert-danger">
            Error: ${error.message}
          </div>
        `;
      }
    });

    // Function to display search results
    function displayResults(data) {
      const totalResults = data.total;

      if (totalResults === 0) {
        results.innerHTML = `
          <div class="no-results">
            <h3>No results found</h3>
            <p>Try a different search term</p>
          </div>
        `;
        return;
      }

      let html = `<h2 class="mb-4">${totalResults} Results Found</h2>`;

      // Performers Section
      if (data.performers && data.performers.length > 0) {
        html += `
          <h3 class="mt-4 mb-3">Performers (${data.performers.length})</h3>
          <div class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-4 mb-5">
        `;

        data.performers.forEach(performer => {
          const imageUrl = performer.image_url || '/static/placeholder.jpg';
          const nationality = performer.nationality ? getFlagEmoji(performer.nationality) : '';

          html += `
            <div class="col">
              <div class="card performer-card">
                <img
                  src="${imageUrl}"
                  class="card-img-top performer-image"
                  alt="${performer.name}"
                  onerror="this.src='/static/placeholder.jpg'"
                >
                <div class="card-body">
                  <h5 class="card-title">${performer.name}</h5>
                  <p class="card-text">
                    ${nationality} ${performer.gender || ''}
                  </p>
                  <a href="/performers/${performer.id}" class="btn btn-sm btn-primary">
                    View Profile
                  </a>
                </div>
              </div>
            </div>
          `;
        });

        html += `</div>`;
      }

      // Studios Section
      if (data.studios && data.studios.length > 0) {
        html += `
          <h3 class="mt-4 mb-3">Studios (${data.studios.length})</h3>
          <div class="list-group mb-5">
        `;

        data.studios.forEach(studio => {
          html += `
            <a href="/studios/${studio.id}" class="list-group-item list-group-item-action">
              <div class="d-flex w-100 justify-content-between">
                <h5 class="mb-1">${studio.name}</h5>
              </div>
              ${studio.description ? `<p class="mb-1">${studio.description}</p>` : ''}
            </a>
          `;
        });

        html += `</div>`;
      }

      // Scenes Section
      if (data.scenes && data.scenes.length > 0) {
        html += `
          <h3 class="mt-4 mb-3">Scenes (${data.scenes.length})</h3>
          <div class="list-group mb-5">
        `;

        data.scenes.forEach(scene => {
          html += `
            <a href="/scenes/${scene.id}" class="list-group-item list-group-item-action">
              <div class="d-flex w-100 justify-content-between">
                <h5 class="mb-1">${scene.title}</h5>
                ${scene.date ? `<small>${scene.date}</small>` : ''}
              </div>
              ${scene.description ? `<p class="mb-1">${scene.description.substring(0, 200)}...</p>` : ''}
            </a>
          `;
        });

        html += `</div>`;
      }

      results.innerHTML = html;
    }

    // Helper function: Convert country code to flag emoji
    function getFlagEmoji(countryCode) {
      const codePoints = countryCode
        .toUpperCase()
        .split('')
        .map(char => 127397 + char.charCodeAt());
      return String.fromCodePoint(...codePoints);
    }
  </script>

</body>
</html>

Performer Import Form

File: import-performer.html

Simple form to search and import performers from TPDB.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Import Performer - Goondex</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

  <style>
    .import-container {
      max-width: 600px;
      margin: 3rem auto;
      padding: 2rem;
    }

    .success-message {
      animation: slideDown 0.3s ease-out;
    }

    @keyframes slideDown {
      from {
        opacity: 0;
        transform: translateY(-10px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
  </style>
</head>
<body>

  <nav class="navbar navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Goondex</a>
    </div>
  </nav>

  <div class="import-container">
    <h1 class="mb-4">Import Performer</h1>

    <div class="card">
      <div class="card-body">
        <p class="card-text text-muted">
          Search for a performer by name. All matching results from TPDB will be imported.
        </p>

        <form id="import-form">
          <div class="mb-3">
            <label for="performer-name" class="form-label">Performer Name</label>
            <input
              type="text"
              class="form-control form-control-lg"
              id="performer-name"
              placeholder="Enter performer name..."
              required
            >
          </div>

          <button type="submit" class="btn btn-primary btn-lg w-100" id="submit-btn">
            <span id="button-text">Import Performer</span>
            <span id="button-spinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
          </button>
        </form>

        <!-- Result message (hidden by default) -->
        <div id="result-message" class="mt-3" style="display: none;"></div>
      </div>
    </div>
  </div>

  <script>
    const API_BASE = 'http://localhost:8080';

    const form = document.getElementById('import-form');
    const input = document.getElementById('performer-name');
    const submitBtn = document.getElementById('submit-btn');
    const buttonText = document.getElementById('button-text');
    const buttonSpinner = document.getElementById('button-spinner');
    const resultMessage = document.getElementById('result-message');

    form.addEventListener('submit', async function(event) {
      event.preventDefault();

      const performerName = input.value.trim();
      if (!performerName) return;

      // Show loading state
      submitBtn.disabled = true;
      buttonText.textContent = 'Importing...';
      buttonSpinner.style.display = 'inline-block';
      resultMessage.style.display = 'none';

      try {
        // Make API request
        const response = await fetch(`${API_BASE}/api/import/performer`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            query: performerName
          })
        });

        const data = await response.json();

        // Reset button
        submitBtn.disabled = false;
        buttonText.textContent = 'Import Performer';
        buttonSpinner.style.display = 'none';

        // Show result
        if (data.success) {
          resultMessage.className = 'alert alert-success success-message mt-3';
          resultMessage.innerHTML = `
            <strong>Success!</strong> ${data.message}<br>
            <small>Imported ${data.data.imported} out of ${data.data.found} found</small>
          `;
          resultMessage.style.display = 'block';

          // Clear form
          input.value = '';

        } else {
          resultMessage.className = 'alert alert-danger mt-3';
          resultMessage.innerHTML = `<strong>Error:</strong> ${data.message}`;
          resultMessage.style.display = 'block';
        }

      } catch (error) {
        submitBtn.disabled = false;
        buttonText.textContent = 'Import Performer';
        buttonSpinner.style.display = 'none';

        resultMessage.className = 'alert alert-danger mt-3';
        resultMessage.innerHTML = `<strong>Error:</strong> ${error.message}`;
        resultMessage.style.display = 'block';
      }
    });
  </script>

</body>
</html>

Bulk Import with Progress

File: bulk-import.html

Import all performers with a real-time progress bar.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Bulk Import - Goondex</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

  <style>
    .import-container {
      max-width: 800px;
      margin: 3rem auto;
      padding: 2rem;
    }

    .progress {
      height: 30px;
    }

    .progress-bar {
      font-size: 14px;
      line-height: 30px;
    }

    .import-stats {
      display: none;
      margin-top: 1rem;
    }

    .stat-box {
      text-align: center;
      padding: 1rem;
      background: #f8f9fa;
      border-radius: 8px;
    }

    .stat-number {
      font-size: 2rem;
      font-weight: bold;
      color: #0d6efd;
    }

    .stat-label {
      color: #6c757d;
      font-size: 0.875rem;
    }
  </style>
</head>
<body>

  <nav class="navbar navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Goondex</a>
    </div>
  </nav>

  <div class="import-container">
    <h1 class="mb-4">Bulk Import Performers</h1>

    <div class="card">
      <div class="card-body">
        <p class="text-muted">
          Import all performers from your database. This will fetch full metadata from TPDB.
        </p>

        <!-- Start Button -->
        <button id="start-btn" class="btn btn-primary btn-lg w-100 mb-4">
          Start Import
        </button>

        <!-- Progress Bar (hidden initially) -->
        <div id="progress-container" style="display: none;">
          <div class="progress mb-2">
            <div
              id="progress-bar"
              class="progress-bar progress-bar-striped progress-bar-animated"
              role="progressbar"
              style="width: 0%"
            >
              0%
            </div>
          </div>

          <div id="status-text" class="alert alert-info">
            Preparing to import...
          </div>

          <!-- Statistics (shown on completion) -->
          <div id="import-stats" class="import-stats">
            <div class="row g-3">
              <div class="col-md-3">
                <div class="stat-box">
                  <div id="stat-total" class="stat-number">0</div>
                  <div class="stat-label">Total</div>
                </div>
              </div>
              <div class="col-md-3">
                <div class="stat-box">
                  <div id="stat-imported" class="stat-number">0</div>
                  <div class="stat-label">Imported</div>
                </div>
              </div>
              <div class="col-md-3">
                <div class="stat-box">
                  <div id="stat-skipped" class="stat-number">0</div>
                  <div class="stat-label">Skipped</div>
                </div>
              </div>
              <div class="col-md-3">
                <div class="stat-box">
                  <div id="stat-errors" class="stat-number">0</div>
                  <div class="stat-label">Errors</div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    const API_BASE = 'http://localhost:8080';

    const startBtn = document.getElementById('start-btn');
    const progressContainer = document.getElementById('progress-container');
    const progressBar = document.getElementById('progress-bar');
    const statusText = document.getElementById('status-text');
    const importStats = document.getElementById('import-stats');

    startBtn.addEventListener('click', function() {
      // Hide button, show progress
      startBtn.style.display = 'none';
      progressContainer.style.display = 'block';
      importStats.style.display = 'none';

      // Create EventSource to listen for progress updates
      const eventSource = new EventSource(
        `${API_BASE}/api/import/all-performers/progress`
      );

      // When we receive a message
      eventSource.onmessage = function(event) {
        const update = JSON.parse(event.data);

        // Check for error
        if (update.error) {
          statusText.className = 'alert alert-danger';
          statusText.textContent = `Error: ${update.error}`;
          progressBar.className = 'progress-bar bg-danger';
          progressBar.style.width = '100%';
          progressBar.textContent = 'Error';
          startBtn.style.display = 'block';
          eventSource.close();
          return;
        }

        // Check for completion
        if (update.complete) {
          const result = update.result;

          statusText.className = 'alert alert-success';
          statusText.innerHTML = `
            <strong>Import Complete!</strong><br>
            Successfully imported ${result.imported} out of ${result.total} performers
          `;

          progressBar.className = 'progress-bar bg-success';
          progressBar.style.width = '100%';
          progressBar.textContent = '100%';

          // Show statistics
          document.getElementById('stat-total').textContent = result.total;
          document.getElementById('stat-imported').textContent = result.imported;
          document.getElementById('stat-skipped').textContent = result.skipped;
          document.getElementById('stat-errors').textContent = result.errors;
          importStats.style.display = 'block';

          startBtn.textContent = 'Import Again';
          startBtn.style.display = 'block';

          eventSource.close();
          return;
        }

        // Progress update
        const percent = Math.round((update.current / update.total) * 100);

        progressBar.style.width = `${percent}%`;
        progressBar.textContent = `${percent}%`;

        statusText.className = 'alert alert-info';
        statusText.innerHTML = `
          <strong>Importing:</strong> ${update.name}<br>
          <small>Progress: ${update.current} / ${update.total}</small>
        `;
      };

      // Handle connection errors
      eventSource.onerror = function() {
        statusText.className = 'alert alert-danger';
        statusText.textContent = 'Connection error. Please try again.';
        progressBar.className = 'progress-bar bg-danger';
        startBtn.style.display = 'block';
        eventSource.close();
      };
    });
  </script>

</body>
</html>

Simple Data Display

File: performers-list.html

Display a list of performers from the database.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Performers - Goondex</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

  <style>
    .performer-grid {
      max-width: 1400px;
      margin: 2rem auto;
      padding: 0 2rem;
    }

    .performer-card {
      height: 100%;
      transition: transform 0.2s;
    }

    .performer-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    }

    .performer-image {
      width: 100%;
      height: 350px;
      object-fit: cover;
    }

    .loading {
      text-align: center;
      padding: 3rem;
    }
  </style>
</head>
<body>

  <nav class="navbar navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Goondex</a>
    </div>
  </nav>

  <div class="performer-grid">
    <h1 class="mb-4">All Performers</h1>

    <!-- Loading indicator -->
    <div id="loading" class="loading">
      <div class="spinner-border text-primary" role="status">
        <span class="visually-hidden">Loading...</span>
      </div>
      <p class="mt-2">Loading performers...</p>
    </div>

    <!-- Performers grid -->
    <div id="performers-grid" class="row row-cols-1 row-cols-md-3 row-cols-lg-5 g-4"></div>
  </div>

  <script>
    const API_BASE = 'http://localhost:8080';

    // Load performers when page loads
    window.addEventListener('load', loadPerformers);

    async function loadPerformers() {
      try {
        // Fetch from search API with empty query (returns all)
        const response = await fetch(`${API_BASE}/api/search?q=`);
        const data = await response.json();

        // Hide loading
        document.getElementById('loading').style.display = 'none';

        if (!data.success) {
          alert('Error: ' + data.message);
          return;
        }

        // Display performers
        displayPerformers(data.data.performers || []);

      } catch (error) {
        document.getElementById('loading').style.display = 'none';
        alert('Error loading performers: ' + error.message);
      }
    }

    function displayPerformers(performers) {
      const grid = document.getElementById('performers-grid');

      if (performers.length === 0) {
        grid.innerHTML = `
          <div class="col-12 text-center text-muted py-5">
            <h3>No performers found</h3>
            <p>Import some performers to get started</p>
          </div>
        `;
        return;
      }

      let html = '';

      performers.forEach(performer => {
        const imageUrl = performer.image_url || '/static/placeholder.jpg';
        const flag = performer.nationality ? getFlagEmoji(performer.nationality) : '';
        const age = calculateAge(performer.birthday);

        html += `
          <div class="col">
            <div class="card performer-card">
              <img
                src="${imageUrl}"
                class="performer-image"
                alt="${performer.name}"
                onerror="this.src='/static/placeholder.jpg'"
              >
              <div class="card-body">
                <h5 class="card-title">${performer.name}</h5>
                <p class="card-text">
                  ${flag} ${performer.gender || ''}<br>
                  ${age ? `Age: ${age}` : ''}
                </p>
                <a href="/performers/${performer.id}" class="btn btn-primary btn-sm w-100">
                  View Profile
                </a>
              </div>
            </div>
          </div>
        `;
      });

      grid.innerHTML = html;
    }

    // Helper: Convert country code to flag emoji
    function getFlagEmoji(countryCode) {
      const codePoints = countryCode
        .toUpperCase()
        .split('')
        .map(char => 127397 + char.charCodeAt());
      return String.fromCodePoint(...codePoints);
    }

    // Helper: Calculate age from birthday
    function calculateAge(birthday) {
      if (!birthday) return null;

      const birthDate = new Date(birthday);
      const today = new Date();
      let age = today.getFullYear() - birthDate.getFullYear();
      const monthDiff = today.getMonth() - birthDate.getMonth();

      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
        age--;
      }

      return age;
    }
  </script>

</body>
</html>

Bootstrap Component Reference

Quick reference for Bootstrap 5 components to use in your pages.

Buttons

<!-- Primary button -->
<button class="btn btn-primary">Primary</button>

<!-- Success button -->
<button class="btn btn-success">Success</button>

<!-- Danger button -->
<button class="btn btn-danger">Danger</button>

<!-- Large button -->
<button class="btn btn-primary btn-lg">Large Button</button>

<!-- Small button -->
<button class="btn btn-primary btn-sm">Small Button</button>

<!-- Full width button -->
<button class="btn btn-primary w-100">Full Width</button>

<!-- Disabled button -->
<button class="btn btn-primary" disabled>Disabled</button>

Alerts

<!-- Success alert -->
<div class="alert alert-success">
  <strong>Success!</strong> Your action was successful.
</div>

<!-- Error alert -->
<div class="alert alert-danger">
  <strong>Error!</strong> Something went wrong.
</div>

<!-- Warning alert -->
<div class="alert alert-warning">
  <strong>Warning!</strong> Please be careful.
</div>

<!-- Info alert -->
<div class="alert alert-info">
  <strong>Info:</strong> Here's some information.
</div>

<!-- Dismissible alert -->
<div class="alert alert-success alert-dismissible fade show">
  Success message
  <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>

Cards

<!-- Basic card -->
<div class="card">
  <img src="image.jpg" class="card-img-top" alt="...">
  <div class="card-body">
    <h5 class="card-title">Card Title</h5>
    <p class="card-text">Card description goes here.</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>
  </div>
</div>

<!-- Card with header -->
<div class="card">
  <div class="card-header">
    Featured
  </div>
  <div class="card-body">
    <h5 class="card-title">Special title</h5>
    <p class="card-text">Card content</p>
  </div>
</div>

Forms

<!-- Basic form -->
<form>
  <div class="mb-3">
    <label for="input1" class="form-label">Email address</label>
    <input type="email" class="form-control" id="input1">
  </div>

  <div class="mb-3">
    <label for="input2" class="form-label">Password</label>
    <input type="password" class="form-control" id="input2">
  </div>

  <button type="submit" class="btn btn-primary">Submit</button>
</form>

<!-- Large input -->
<input type="text" class="form-control form-control-lg" placeholder="Large input">

<!-- Small input -->
<input type="text" class="form-control form-control-sm" placeholder="Small input">

<!-- Select dropdown -->
<select class="form-select">
  <option selected>Choose...</option>
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
</select>

Progress Bars

<!-- Basic progress bar -->
<div class="progress">
  <div class="progress-bar" style="width: 50%">50%</div>
</div>

<!-- Colored progress bars -->
<div class="progress">
  <div class="progress-bar bg-success" style="width: 75%"></div>
</div>

<div class="progress">
  <div class="progress-bar bg-danger" style="width: 100%"></div>
</div>

<!-- Striped and animated -->
<div class="progress">
  <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 75%"></div>
</div>

Spinners

<!-- Basic spinner -->
<div class="spinner-border" role="status">
  <span class="visually-hidden">Loading...</span>
</div>

<!-- Colored spinners -->
<div class="spinner-border text-primary"></div>
<div class="spinner-border text-success"></div>
<div class="spinner-border text-danger"></div>

<!-- Small spinner -->
<div class="spinner-border spinner-border-sm"></div>

<!-- Growing spinner -->
<div class="spinner-grow" role="status">
  <span class="visually-hidden">Loading...</span>
</div>

Lists

<!-- Basic list group -->
<ul class="list-group">
  <li class="list-group-item">Item 1</li>
  <li class="list-group-item">Item 2</li>
  <li class="list-group-item">Item 3</li>
</ul>

<!-- Clickable list -->
<div class="list-group">
  <a href="#" class="list-group-item list-group-item-action">
    Clickable item
  </a>
  <a href="#" class="list-group-item list-group-item-action active">
    Active item
  </a>
</div>

Grid System

<!-- Responsive grid (1 column on mobile, 3 on tablet, 4 on desktop) -->
<div class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-4">
  <div class="col">Column 1</div>
  <div class="col">Column 2</div>
  <div class="col">Column 3</div>
  <div class="col">Column 4</div>
</div>

<!-- Two column layout -->
<div class="row">
  <div class="col-md-6">Left column</div>
  <div class="col-md-6">Right column</div>
</div>

<!-- Sidebar layout -->
<div class="row">
  <div class="col-md-3">Sidebar</div>
  <div class="col-md-9">Main content</div>
</div>

CSS Tips

Spacing Utilities

<!-- Margin -->
<div class="m-3">Margin on all sides</div>
<div class="mt-3">Margin top</div>
<div class="mb-3">Margin bottom</div>
<div class="mx-3">Margin left and right</div>
<div class="my-3">Margin top and bottom</div>

<!-- Padding -->
<div class="p-3">Padding on all sides</div>
<div class="pt-3">Padding top</div>
<div class="pb-3">Padding bottom</div>
<div class="px-3">Padding left and right</div>
<div class="py-3">Padding top and bottom</div>

<!-- Sizes: 0, 1, 2, 3, 4, 5 (0 = 0, 5 = 3rem) -->

Text Utilities

<!-- Alignment -->
<div class="text-start">Left aligned</div>
<div class="text-center">Center aligned</div>
<div class="text-end">Right aligned</div>

<!-- Colors -->
<p class="text-primary">Primary text</p>
<p class="text-success">Success text</p>
<p class="text-danger">Danger text</p>
<p class="text-muted">Muted text</p>

<!-- Font weight -->
<p class="fw-bold">Bold text</p>
<p class="fw-normal">Normal text</p>
<p class="fw-light">Light text</p>

<!-- Font size -->
<p class="fs-1">Very large</p>
<p class="fs-5">Medium</p>

Display Utilities

<!-- Show/Hide -->
<div class="d-none">Hidden</div>
<div class="d-block">Shown as block</div>
<div class="d-inline-block">Inline block</div>
<div class="d-flex">Flexbox container</div>

<!-- Responsive display -->
<div class="d-none d-md-block">Hidden on mobile, shown on tablet+</div>

Quick JavaScript Snippets

Show/Hide Elements

// Hide element
document.getElementById('myElement').style.display = 'none';

// Show element
document.getElementById('myElement').style.display = 'block';

// Toggle visibility
const el = document.getElementById('myElement');
el.style.display = el.style.display === 'none' ? 'block' : 'none';

Change Text Content

document.getElementById('myElement').textContent = 'New text';
document.getElementById('myElement').innerHTML = '<strong>HTML</strong> text';

Add/Remove CSS Classes

// Add class
document.getElementById('myElement').classList.add('active');

// Remove class
document.getElementById('myElement').classList.remove('active');

// Toggle class
document.getElementById('myElement').classList.toggle('active');

Get Form Values

const inputValue = document.getElementById('myInput').value;
const selectValue = document.getElementById('mySelect').value;

Need Help?

All the JavaScript in these templates is ready to use - just copy and paste! If you need to customize something:

  1. Look for comments in the JavaScript (// like this)
  2. The API_BASE constant at the top controls the server URL
  3. Use AI assistance to modify the JavaScript parts
  4. Focus on the HTML/CSS which you're already great at!

The templates are fully functional and follow Bootstrap best practices.