top of page

How Google Gemini and Apps Script Saved My Wedding RSVP Sanity

As an admitted "Type A" bride-to-be, I recently explored using Google Gemini to write Apps Script and discovered a fantastic application I'm excited to share: a custom Apps Script solution for managing wedding RSVPs.


The Challenge

I designed a custom wedding website using Wix, as I love designing sites. However, wedding planning often involves juggling multiple software platforms and spreadsheets. I needed a way to manage RSVPs directly in a spreadsheet without the cumbersome steps of designing a separate form, exporting responses, importing them, and then uploading them into my wedding planning software.I  wanted complete control over the form's design to ensure it perfectly matched my website's color palette, fonts, and overall aesthetic.


My goal was to create a beautiful, embedded form that would seamlessly connect to a Google Sheet for real-time RSVP tracking.


The Solution


I achieved this by utilizing a Google Apps Script Web App integrated with a Google Sheet. I leveraged Gemini to rapidly develop the form within the Web App, which I then embedded into my password-protected Wix website. Now, every RSVP response is immediately recorded in the Google Sheet, and I receive a detailed email notification for each submission.


How it Works


The guest list is centrally managed in a Google Sheet, with households grouped by a unique ID, allowing one person to RSVP for the entire household.


  1. Name Entry & Household Lookup: A guest enters their name, and the Apps Script automatically searches for and displays all household members on the form.

  2. Form Completion: The guest fills out the form for each individual. The meal selection area is dynamic, displaying meal choice descriptions upon selection.

  3. Submission & Celebration: When the form is submitted, if at least one "yes" response is recorded, a fun confetti animation appears on the screen. 🎉

  4. Immediate Tracking: The response is instantly logged in the Google Sheet, and an email summarizing the details is sent to me.



RSVP Notification Email
RSVP Notification Email

Building the Web App with Gemini


I am not an expert in Javascript or HTML. I relied on Gemini to write and fine-tune all the necessary code. I enjoy using Gemini for learning; while it's not flawless, neither am I! As a visual learner intimidated by code, using Gemini for this real-world project has made me much more comfortable and familiar with programming concepts.


Code Preview:


Code.gs

function doGet() {
  return HtmlService.createTemplateFromFile('Index')
      .evaluate()
      .addMetaTag('viewport', 'width=device-width, initial-scale=1')
      .setTitle('Wedding RSVP')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function searchGuests(firstName, lastName) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Guests");
  if (!sheet) throw new Error("Sheet 'Guests' not found!");
  const data = sheet.getDataRange().getValues();
  data.shift(); 
  const match = data.find(row => 
    row[0].toString().toLowerCase().trim() === firstName.toLowerCase().trim() &&
    row[1].toString().toLowerCase().trim() === lastName.toLowerCase().trim()
  );
  if (!match) return [];
  const householdId = match[2];
  return data.filter(row => row[2] === householdId);
}

function saveRSVPs(responses) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Guests");
  const data = sheet.getDataRange().getValues();
  responses.forEach(res => {
    for (let i = 1; i < data.length; i++) {
      if (data[i][0] === res.firstName && data[i][1] === res.lastName) {
        let row = i + 1;
        sheet.getRange(row, 4).setValue(res.attending);
        if (res.attending === "Yes") {
          sheet.getRange(row, 5).setValue(res.meal);
          sheet.getRange(row, 6).setValue(res.dietary);
          sheet.getRange(row, 7).setValue(res.transport);
          sheet.getRange(row, 8).setValue(res.lodging);
          sheet.getRange(row, 9).setValue(res.welcomeParty);
        } else {
          sheet.getRange(row, 5, 1, 5).clearContent();
        }
      }
    }
  });
  sendAdminNotification(responses);
  return "Success";
}

function sendAdminNotification(responses) {
  const myEmail = "your-email@gmail.com"; 
  const familyName = responses[0].lastName;
  const subject = "Wedding RSVP: " + responses[0].firstName + " " + familyName;
  let body = "New RSVP submission received:\n\n";
  responses.forEach(res => {
    body += `Guest: ${res.firstName} ${res.lastName} - ${res.attending}\n`;
    if (res.attending === "Yes") {
      body += `Meal: ${res.meal} | Diet: ${res.dietary || "None"}\n`;
      body += `Lodging: ${res.lodging}\n`;
      body += `Shuttle: ${res.transport} | Welcome Party: ${res.welcomeParty}\n`;
    }
    body += "----------------------------\n";
  });
  MailApp.sendEmail(myEmail, subject, body);
}

Index.html


<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Aboreto&family=Inter:wght@400;500&display=swap" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
    
    <style>
      body { background-color: transparent; font-family: 'Inter', sans-serif; padding: 20px; }
      .rsvp-card { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
      h1 { font-family: 'Aboreto', cursive; color: #5c6358; letter-spacing: 2px; }
      .date-subtitle { color: #a89f91; font-size: 0.85rem; margin-bottom: 30px; text-transform: uppercase; letter-spacing: 1px; }
      .guest-section { border-bottom: 1px solid #f2e9e4; padding-bottom: 25px; margin-bottom: 25px; text-align: left; }
      .guest-name { font-family: 'Aboreto', cursive; font-size: 1.1rem; color: #4a4a4a; margin-bottom: 15px; }
      
      .btn-primary { 
        background-color: #d8a48f !important; 
        border: none !important; 
        border-radius: 8px; 
        padding: 14px;
        transition: all 0.3s ease;
        color: white !important;
      }
      .btn-primary:hover, .btn-primary:active, .btn-primary:focus { 
        background-color: #c98a75 !important; 
      }
      .btn-primary:disabled {
        background-color: #e5c1b3 !important; 
        opacity: 0.8;
      }

      .form-control, .form-select { border-radius: 8px; margin-bottom: 15px; border: 1px solid #e2e2e2; -webkit-appearance: none; }
      .form-check-input:checked { background-color: #b7c4b0; border-color: #b7c4b0; }
      .hidden { display: none; }
      .meal-description {
        font-size: 0.8rem;
        color: #777;
        font-style: italic;
        margin-top: -10px;
        margin-bottom: 15px;
        line-height: 1.4;
      }
      .spinner-border { width: 1.2rem; height: 1.2rem; margin-right: 8px; display: none; vertical-align: middle; }
    </style>
  </head>
  <body>

    <div class="container text-center">
      <div class="rsvp-card">
        <h1>Our Wedding</h1>
        <p class="date-subtitle">Kindly respond by July 1st, 2026</p>
        <hr class="mb-4">

        <div id="search-container">
          <p class="mb-4">Please enter your name to find your invitation:</p>
          <input type="text" id="firstNameInput" class="form-control" placeholder="First Name" autocomplete="given-name">
          <input type="text" id="lastNameInput" class="form-control" placeholder="Last Name" autocomplete="family-name">
          <button id="searchBtn" class="btn btn-primary w-100" onclick="handleSearch()">
            <span id="searchSpinner" class="spinner-border spinner-border-sm" role="status"></span>
            Find Invitation
          </button>
        </div>

        <div id="form-container" class="hidden">
          <form id="rsvpForm">
            <div id="guestList"></div>
            <button id="submitBtn" type="button" class="btn btn-primary w-100 mt-3" onclick="submitRSVPs()">
              <span id="submitSpinner" class="spinner-border spinner-border-sm" role="status"></span>
              Submit RSVP
            </button>
          </form>
        </div>

        <div id="success-message" class="hidden">
          <h2 style="font-family: 'Aboreto'; color: #d8a48f;">Thank you!</h2>
          <p class="mt-3">Your response has been saved. We can't wait to celebrate with you!</p>
        </div>
      </div>
    </div>

    <script>
      let currentGuests = [];
      const colors = ['#b7c4b0', '#f4d3d3', '#d8a48f', '#a89f91'];
      
      const menuDetails = {
        "Steak": "WAGYU FILET (GF): Bone Marrow, Mushroom Fricassee, Potato Puree, and Bordelaise Sauce.",
        "Salmon": "MISO GLAZED SALMON FILET: Garlic-Chili sauteed Broccolini with buttery Jasmine Rice topped with Orange-infused Sweet Chili Drizzle"
      };

      /**
       * MOBILE KEYBOARD FIX: 
       * Forces focus on touch for iFrame environments.
       */
      document.addEventListener('touchstart', function(e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
          e.target.focus();
        }
      }, {passive: true});

      function handleSearch() {
        const fn = document.getElementById('firstNameInput').value;
        const ln = document.getElementById('lastNameInput').value;
        if (!fn || !ln) return alert("Please enter both first and last name.");
        
        document.getElementById('searchBtn').disabled = true;
        document.getElementById('searchSpinner').style.display = 'inline-block';
        
        google.script.run
          .withSuccessHandler(displayGuests)
          .withFailureHandler(handleError)
          .searchGuests(fn, ln);
      }

      function handleError(err) {
        alert("Error: " + err);
        resetButtons();
      }

      function displayGuests(guests) {
        resetButtons();
        if (!guests || guests.length === 0) {
          alert("Invitation not found. Please check your spelling.");
          return;
        }
        currentGuests = guests;
        document.getElementById('search-container').classList.add('hidden');
        const listDiv = document.getElementById('guestList');
        listDiv.innerHTML = '';

        guests.forEach((guest, index) => {
          listDiv.innerHTML += `
            <div class="guest-section">
              <p class="guest-name"><strong>${guest[0]} ${guest[1]}</strong></p>
              
              <label class="form-label small fw-bold">Will you be attending?</label>
              <select id="attending-${index}" class="form-select mb-3" onchange="toggleDetails(${index}, this.value)">
                <option value="">Select...</option>
                <option value="Yes">Yes, gladly</option>
                <option value="No">No, regrettably</option>
              </select>

              <div id="details-${index}" class="hidden">
                <label class="form-label small fw-bold">Meal Choice</label>
                <select id="meal-${index}" class="form-select mb-3" onchange="updateMealDesc(${index}, this.value)">
                  <option value="Salmon">Miso Glazed Salmon</option>
                  <option value="Steak">Wagyu Filet</option>
                </select>
                <div id="meal-desc-${index}" class="meal-description">${menuDetails['Salmon']}</div>

                <label class="form-label small fw-bold">Dietary Restrictions</label>
                <input type="text" id="diet-${index}" class="form-control mb-3" placeholder="None">

                <label class="form-label small fw-bold">Where are you staying?</label>
                <select id="lodging-${index}" class="form-select mb-3">
                 <option value="Seabird Resort">Seabird Resort</option>
                 <option value="Mission Pacific Beach Resort">Mission Pacific Beach Resort</option>
                  <option value="SpringHill Suites">SpringHill Suites</option>
                  <option value="Airbnb">Airbnb</option>
                  <option value="Other">Other</option>
                </select>

                <div class="form-check mb-2">
                  <input id="transport-${index}" class="form-check-input" type="checkbox">
                  <label class="form-check-label small" for="transport-${index}">Plan to use provided shuttle to ceremony</label>
                </div>

                <div class="form-check mb-2">
                  <input id="welcome-${index}" class="form-check-input" type="checkbox">
                  <label class="form-check-label small" for="welcome-${index}">Plan to attend Night Before Welcome Party</label>
                </div>
              </div>
            </div>
          `;
        });
        document.getElementById('form-container').classList.remove('hidden');
      }

      function updateMealDesc(index, value) {
        document.getElementById(`meal-desc-${index}`).innerText = menuDetails[value];
      }

      function toggleDetails(index, value) {
        document.getElementById(`details-${index}`).classList.toggle('hidden', value !== 'Yes');
      }

      function submitRSVPs() {
        document.getElementById('submitBtn').disabled = true;
        document.getElementById('submitSpinner').style.display = 'inline-block';
        
        const responses = currentGuests.map((guest, i) => ({
          firstName: guest[0], lastName: guest[1],
          attending: document.getElementById(`attending-${i}`).value,
          meal: document.getElementById(`meal-${i}`).value,
          dietary: document.getElementById(`diet-${i}`).value,
          lodging: document.getElementById(`lodging-${i}`).value,
          transport: document.getElementById(`transport-${i}`).checked ? "Yes" : "No",
          welcomeParty: document.getElementById(`welcome-${i}`).checked ? "Yes" : "No"
        }));

        const anyAttending = responses.some(r => r.attending === "Yes");

        google.script.run.withSuccessHandler(() => {
          document.getElementById('form-container').classList.add('hidden');
          document.getElementById('success-message').classList.remove('hidden');
          if (anyAttending) triggerConfetti();
        }).saveRSVPs(responses);
      }

      function triggerConfetti() {
        const end = Date.now() + 3000;
        (function frame() {
          confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0 }, colors: colors });
          confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1 }, colors: colors });
          if (Date.now() < end) requestAnimationFrame(frame);
        }());
      }

      function resetButtons() {
        if(document.getElementById('searchBtn')) document.getElementById('searchBtn').disabled = false;
        if(document.getElementById('submitBtn')) document.getElementById('submitBtn').disabled = false;
        document.querySelectorAll('.spinner-border').forEach(s => s.style.display = 'none');
      }
    </script>
  </body>
</html>

Conclusion


Using Gemini to build this RSVP solution was not only enjoyable but also incredibly time-saving and headache-reducing. Any time saved during wedding planning is a definite win!


 
 
 

Comments


  • LinkedIn
  • Twitter
Jessica Mills Profile Picture.jpg

About Me

My name is Jessica Mills and I am a Sales Operations Associate at Snap, Inc. I started learning Salesforce when I was 18 years old and a freshman in college. I wouldn't be where I am today without the guidance and support of others. My hope is to help people navigate the Salesforce ecosystem.

 

Read More

 

bottom of page