สร้างเว็บแอป วันแม่ + เกียรติบัตร (Mother's Day)

DEMO


 วิธีใช้

 1. เปิดกูเกิลชีต สร้างชีต ชื่อ data

2. สร้างหัวตาราง
timestampชื่อ นามสกุลหน่วยงานจังหวัดข้อความ

3. เปิด ส่วนขยาย > Apps Script

4. สร้างไฟล์ และคัดลอกโค้ดด้านล่าง

5. รูปหลัก จะใช้ขนาด ความสูงไม่ต่ำกว่า 620 พิกเซล ความกว้าง 1035 พิกเซล รูปจะจัดกลาง และชิดขอบบน

6. การเปลี่ยนรูปหลัก (index.html)
.hero-section {
      background: linear-gradient(#a3a2a200, #f7f5f500),
        url('https://semicon.github.io/img/web/mothersDay.png') top center no-repeat;
      color: hsla(0, 0%, 100%, .622);
      overflow: hidden;
      padding: 80px 0 0 0;
      position: relative;
      height: 620px;
    }

7. การเปลี่ยนรูปพื้นหลังการ์ด  (index.html)
.card-bg {
      background: linear-gradient(#ececec33, #fefefe33),
        url('https://semicon.github.io/img/web/jasmin.png');
      background-size: cover;
      background-position: center;
      color: blue;
    }

8.เปลี่ยนไอดีชีต  (code.gs)
// ID Google Sheets
const SPREADSHEET_ID = 'xxxxxxxxxxxxxxxxxxxxxxx'; // <- แทนที่ตรงนี้

9. เปลี่ยนชื่อชีต (ถ้าต้องการ)  (code.gs)
const sheetName = 'data'; // <- แทนที่ชื่อชีทถ้าจำเป็น

10. เปลี่ยนพื้นหลังเกียรติบัตร (index.html)
/* เกียรติบัตร */
    .modal-content {
      background-image: url('https://semicon.github.io/img/web/bg_cert.png');
      /* พื้นหลัง */
      background-size: center;
      background-position: top;
      background-repeat: no-repeat;
    } /* @media print */
body {
        background-image: url('https://semicon.github.io/img/web/bg_cert.png');
        /* Center and scale the image nicely */
        background-position: center;
        background-repeat: no-repeat;
        background-size: cover;
        left: 0;
        top: 0;
        margin: 0;
        width: 100%;
        height: 100%;
      }

11. เปลี่ยนรูปโลโก้  (index.html <!-- Modal แสดงเกียรติบัตร -->)
<img src="https://semicon.github.io/img/web/logoxxx.png" class="logo" alt="Logo">

12. เปลี่ยนรูปลายเซ็น  (index.html <!-- Modal แสดงเกียรติบัตร -->)
<img src="https://semicon.github.io/img/web/sign3.png" alt="sign" style=" height:60px;">
13. ถ้าต้องการใช้รูปจากกูเกิลไดรฟให้ใช้ URL ด้านข้างนี้
https://lh3.googleusercontent.com/d/ไอดีของรูปภาพที่จะใช้ในกูเกิลไดรฟ
เช่น 
https://lh3.googleusercontent.com/d/1UC0cAJdipsckgFFpcmIH2wzZ7ymazrDc

14. การพิมพ์เกียรติบัตร


โค้ดทั้งหมด

code.gs

// ID Google Sheets
const SPREADSHEET_ID = 'xxxxxxxxxxxxxxxxxxxxxxx'; // <- แทนที่ตรงนี้

function doGet() {
  return HtmlService.createTemplateFromFile('index').evaluate()
    .setTitle("Project Kru Chian")
    .setFaviconUrl("https://semicon.github.io/img/logo2small.png")
    .addMetaTag('viewport', 'width=device-width , initial-scale=1')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .setSandboxMode(HtmlService.SandboxMode.IFRAME)
}

function include(file){
  return HtmlService.createHtmlOutputFromFile(file).getContent()
}

/**
 * ดึงข้อมูลจาก Google Sheet แล้วคืนเป็น array ของ object
 * ปรับ SPREADSHEET_ID และ sheetName ตามจริง
 */
function getSheetData() {
 
  const sheetName = 'data'; // <- แทนที่ชื่อชีทถ้าจำเป็น
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(sheetName);
  if (!sheet) return [];
  const values = sheet.getDataRange().getDisplayValues();
  if (values.length <= 1) return []; // ไม่มีข้อมูลหลัง header

  const headers = values[0].map(h => h.toString().trim().toLowerCase());
  const rows = values.slice(1).reverse();

  // สมมติคอลัมน์เรียง: timestamp | ชื่อ นามสกุล | หน่วยงาน | จังหวัด | ข้อความ
  return rows.map(r => ({
    timestamp: r[0],
    fullname: r[1],
    unit: r[2],
    province: r[3],
    message: r[4]
  }));
}

// บันทึกข้อมูลลงชีต
function saveToSheet(data) {
  try {
    const sheetName = 'data';
    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheet = ss.getSheetByName(sheetName);

    const now = new Date();
    sheet.appendRow([
      Utilities.formatDate(now, Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss"),
      data.fullname,
      data.unit,
      data.province,
      data.message
    ]);

    return { success: true, message: 'บันทึกข้อมูลเรียบร้อย' };
  } catch (err) {
    return { success: false, message: err.toString() };
  }
}

index.html

<!DOCTYPE html>
<html lang="th">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>การ์ดจาก Google Sheet</title>

  <!-- Bootstrap 5.3 CDN (ตามที่คุณชอบใช้ CDN) -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">

  <!-- Bootstrap JS (ไม่จำเป็นแต่เก็บไว้) -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script>

  <style>
    @import url('https://fonts.googleapis.com/css2?family=Sriracha&display=swap');

    body {
      font-family: "Sriracha", sans-serif;
      background: linear-gradient(135deg, rgba(7, 181, 250, 1) 0%, rgba(116, 221, 247, 1) 20%, rgba(186, 234, 247, 1) 50%, rgba(116, 221, 247, 1) 80%, rgba(7, 181, 250, 1) 100%);
    }

    .card {
      margin-bottom: 16px;
    }

    .border-l {
      border-left: 5px solid #33d;
    }

    #pagination .page-item .page-link {
      cursor: pointer;
    }

    .card-message {
      white-space: pre-wrap;
    }

    .card-bg {
      background: linear-gradient(#ececec33, #fefefe33),
        url('https://semicon.github.io/img/web/jasmin.png');
      background-size: cover;
      background-position: center;
      color: blue;
    }

    h6.card-subtitle {
      border-bottom: 1px dashed #555;
    }

    .hero-section {
      background: linear-gradient(#a3a2a200, #f7f5f500),
        url('https://semicon.github.io/img/web/mothersDay.png') top center no-repeat;
      color: hsla(0, 0%, 100%, .622);
      overflow: hidden;
      padding: 80px 0 0 0;
      position: relative;
      height: 620px;
    }

    .text-title {
      margin-top: 340px;
      margin-bottom: 50px;
    }

    .svg-text {
      text-shadow:
        1px 0 0 #ccc, -1px 0 0 #fff,
        0 1px 0 #ccc, 0 -1px 0 #fff,
        2px 0 0 #ccc, -2px 0 0 #fff,
        0 2px 0 #ccc, 0 -2px 0 #fff,
        1px 1px 0 #ccc, -1px -1px 0 #fff,
        1px -1px 0 #ccc, -1px 1px 0 #fff;
    }

    /* loader */
    .overlay-loader {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      background: rgba(255, 255, 255, 0.8);
      z-index: 999;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 1.2rem;
      display: none;
      backdrop-filter: blur(4px);
    }

    .loader-text {
      font-family: "Sriracha", cursive;
      margin-top: 15px;
      color: #333;
    }

    .spinner-border.gradient {
      width: 4rem;
      height: 4rem;
      border-width: 0.25em;
      border-style: solid;
      border-color: transparent;
      border-top-color: #fff0;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      background: conic-gradient(red, orange, yellow, green, cyan, blue, violet, red);
      -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 0.3em), black calc(100% - 0.3em));
      mask: radial-gradient(farthest-side, transparent calc(100% - 0.3em), black calc(100% - 0.3em));
    }

    .no-scroll {
      overflow: hidden !important;
    }

    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }

    /* เกียรติบัตร */
    .modal-content {
      background-image: url('https://semicon.github.io/img/web/bg_cert.png');
      /* พื้นหลัง */
      background-size: center;
      background-position: top;
      background-repeat: no-repeat;
    }

    #certificate-content {
      text-align: center;
      font-family: "Sriracha", sans-serif;
      color: #000;
    }

    img.logo {
      margin-top: 100px;
      width: 100px;
    }

    .cer-title {
      font-size: 26pt;
      font-weight: bold;
      padding: 20px 0px;
    }

    .cer-text {
      font-size: 16pt;
    }

    .nowrap {
      white-space: nowrap;
    }

    .sign {
      margin-top: 50px;
    }

    @media print {
      body * {
        visibility: hidden;
      }

      body {
        background-image: url('https://semicon.github.io/img/web/bg_cert.png');
        /* Center and scale the image nicely */
        background-position: center;
        background-repeat: no-repeat;
        background-size: cover;
        left: 0;
        top: 0;
        margin: 0;
        width: 100%;
        height: 100%;
      }

      #certificate,
      #certificate * {
        visibility: visible;
      }

      #certificate {
        position: absolute;
        width: 100%;
        height: 100%;
      }

      .container {
        display: none;
      }

      .modal-footer {
        display: none;
      }

      #certificate-content {
        color: #000;
        white-space: nowrap;
        padding: 0px !important;
        margin: 0px !important;
      }

      #certificate-content img {
        width: 100px;
        margin-top: 30px;
      }

      .sign {
        margin-top: 7px !important;
      }
    }
  </style>
</head>

<body>

  <div class="hero-section">
    <div class="container">
      <div class="hero-content text-center">
        <div class="text-title">
          <h2 class="text-primary fw-bold mb-3 svg-text">ลงนามถวายพระพรชัยมงคล</h2>
          <h3 class="text-primary mb-4 svg-text nowrap">สมเด็จพระนางเจ้าสิริกิติ์ พระบรมราชินีนาถ พระบรมราชชนนีพันปีหลวง
          </h3>
          <p class="lead text-primary svg-text">เนื่องในโอกาสวันเฉลิมพระชนมพรรษา วันที่ ๑๒ สิงหาคม ๒๕๖๘</p>
        </div>
      </div>
    </div>
  </div>

  <div class="container">
    <div id="viewer" style="display: block;">
      <div class="mt-4 text-center p-3">
        <a href="#" class="btn btn-primary btn-lg me-3" onclick="toggleDiv('formSubmit'); event.preventDefault();">
          <i class="bi bi-pen"></i>
          ลงนามถวายพระพร
        </a>
      </div>
      <h3 class="mb-4">รายชื่อผู้ลงนามถวายพระพร</h3>

      <!-- ช่องแสดงการ์ด -->
      <div id="cardsRow" class="row"></div>

      <!-- ถ้ายังไม่มีข้อมูล -->
      <div id="noData" class="text-center text-muted" style="display:none;">
        ยังไม่มีข้อมูล
      </div>

      <!-- เพจจิเนชั่น -->
      <nav aria-label="Page navigation" class="pt-3">
        <ul id="pagination" class="pagination justify-content-center"></ul>
      </nav>
    </div>

    <div id="formSubmit" class="mt-5" style="display: none;">

      <?!= include('form') ?>

    </div>
  </div>

  <footer class="bg-primary text-center text-light p-3 mt-5">
    Copyright © 2025 KruChian
  </footer>

  <!-- ****** Preloader Overlay ****** -->
  <div id="overlayLoader" class="overlay-loader">
    <div class="spinner-border gradient" role="status">
      <span class="visually-hidden">กำลังโหลด...</span>
    </div>
    <div class="loader-text text-danger">กำลังประมวลผล กรุณารอสักครู่...</div>
  </div>

  <!-- Modal แสดงเกียรติบัตร -->
  <div class="modal fade" id="certificateModal" tabindex="-1">
    <div class="modal-dialog modal-fullscreen modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-body">
          <div id="certificate">
            <div id="certificate-content">
              <img src="https://semicon.github.io/img/web/logoxxx.png" class="logo" alt="Logo">
              <div class="cer-title mx-3">โรงเรียนวิเชียรวิทยา</div>
              <div class="cer-text">ขอมอบเกียรติบัตรฉบับนี้ให้ไว้ เพื่อแสดงว่า</div>
              <div class="cer-title mx-2" id="cer-name"></div>
              <div class="cer-text ">
                ได้ร่วมลงนามถวายพระพร<br>เนื่องในโอกาสวันเฉลิมพระชนมพรรษา<br>สมเด็จพระนางเจ้าสิริกิติ์ พระบรมราชินีนาถ พระบรมราชชนนีพันปีหลวง<br>
              </div>
              <div class="cer-text">เมื่อวันที่ <span id="cer-date"></span></div>
              <div class="cer-text sign">
                <img src="https://semicon.github.io/img/web/sign3.png" alt="sign" style=" height:60px;">
              </div>
              <div class="cer-text">(นายวิเชียร พุ่มพวง)</div>
              <div class="cer-text">ผู้อำนวยการ</div>
            </div>
          </div>
        </div>
        <div class="modal-footer" style="justify-content: center;">
          <button class="btn btn-primary me-3" onclick="printCertificate()">พิมพ์เกียรติบัตร</button>
          <button class="btn btn-danger" data-bs-dismiss="modal">ปิด</button>
        </div>
      </div>
    </div>
  </div>


  <script>
    let allData = [];
    const pageSize = 9;
    let currentPage = 1;

    function loadData() {
      google.script.run.withSuccessHandler(function(data){
        allData = data || [];
        if (allData.length === 0) {
          document.getElementById('noData').style.display = 'block';
          document.getElementById('cardsRow').innerHTML = '';
          document.getElementById('pagination').innerHTML = '';
          hideLoader()
        } else {
          document.getElementById('noData').style.display = 'none';
          renderPage(1);
          renderPagination();
        }
      }).getSheetData();
    }

    function renderPage(page) {
      currentPage = page;
      const start = (page - 1) * pageSize;
      const pageItems = allData.slice(start, start + pageSize);

      const row = document.getElementById('cardsRow');
      row.innerHTML = '';

      pageItems.forEach((item, index) => {
        const col = document.createElement('div');
        col.className = 'col-12 col-md-6 col-lg-4 py-2';

        const card = document.createElement('div');
        card.className = 'card h-100';
        card.classList.add('card-bg');

        if (index % 2 === 0) {
          card.style.borderLeft = '5px solid #0D6EFD';
        } else {
          card.style.borderLeft = '5px solid #3a3';
        }

        const cardBody = document.createElement('div');
        cardBody.className = 'card-body d-flex flex-column';

        const title = document.createElement('h5');
        title.className = 'card-title';
        title.className = 'pt-2';
        title.innerHTML = `<i class="bi bi-person-vcard"></i> ${item.fullname}` || '-';

        const sub = document.createElement('h6');
        sub.className = 'card-subtitle pb-2 text-dark';
        sub.innerHTML = `<i class="bi bi-buildings"></i> ${item.unit || '-'} <br><i class="bi bi-geo-alt"></i> ${item.province || '-'}`;

        const msg = document.createElement('p');
        msg.className = 'card-message mt-3 mb-0';
        msg.textContent = item.message || '';

        const time = document.createElement('small');
        time.className = 'text-muted text-end pt-2';
        time.textContent = item.timestamp || '';

        const btn = document.createElement('button');
        btn.className = 'btn btn-success  btn-sm mt-3';
        btn.innerHTML = '<i class="bi bi-award"></i> ดูเกียรติบัตร';
        btn.addEventListener('click', () => showCertificate(item));

        cardBody.appendChild(title);
        cardBody.appendChild(sub);
        cardBody.appendChild(msg);
        cardBody.appendChild(time);
        cardBody.appendChild(btn);

        card.appendChild(cardBody);
        col.appendChild(card);
        row.appendChild(col);
      });

      updatePaginationActive();
      window.scrollTo({ top: 0, behavior: 'smooth' });
      hideLoader()
    }

    function renderPagination() {
      const total = allData.length;
      const totalPages = Math.ceil(total / pageSize);
      const pag = document.getElementById('pagination');
      pag.innerHTML = '';

      if (totalPages <= 1) return;

      const prevLi = document.createElement('li');
      prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
      prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" aria-label="Previous">&laquo;</a>`;
      prevLi.addEventListener('click', () => { if (currentPage > 1) renderPage(currentPage - 1); });
      pag.appendChild(prevLi);

      for (let i = 1; i <= totalPages; i++) {
        const li = document.createElement('li');
        li.className = 'page-item' + (i === currentPage ? ' active' : '');
        li.innerHTML = `<a class="page-link" href="javascript:void(0)">${i}</a>`;
        li.addEventListener('click', () => renderPage(i));
        pag.appendChild(li);
      }

      const nextLi = document.createElement('li');
      nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
      nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" aria-label="Next">&raquo;</a>`;
      nextLi.addEventListener('click', () => { if (currentPage < totalPages) renderPage(currentPage + 1); });
      pag.appendChild(nextLi);
    }

    function updatePaginationActive() {
      const pag = document.getElementById('pagination');
      if (!pag) return;
      const items = pag.querySelectorAll('.page-item');
      items.forEach(li => li.classList.remove('active'));
      const pageLinks = pag.querySelectorAll('.page-link');
      pageLinks.forEach(link => {
        if (parseInt(link.textContent) === currentPage) {
          link.parentElement.classList.add('active');
        }
      });

      const totalPages = Math.ceil(allData.length / pageSize);
      if (pageLinks.length > 0) {
        const firstLi = pag.querySelector('li:first-child');
        const lastLi = pag.querySelector('li:last-child');
        if (firstLi) firstLi.classList.toggle('disabled', currentPage === 1);
        if (lastLi) lastLi.classList.toggle('disabled', currentPage === totalPages);
      }
    }

   
    function toggleDiv(idToShow) {
      const viewer = document.getElementById('viewer');
      const formSubmit = document.getElementById('formSubmit');

      if (idToShow === 'formSubmit') {
        viewer.style.display = 'none';
        formSubmit.style.display = 'block';
      } else {
        viewer.style.display = 'block';
        formSubmit.style.display = 'none';
      }
    }

    document.getElementById('dataForm').addEventListener('submit', function(e) {
        e.preventDefault();
        showLoader()
        const formData = Object.fromEntries(new FormData(this).entries());

        google.script.run.withSuccessHandler(function(res) {
          const status = document.getElementById('status');
          if (res.success) {
            status.innerHTML = `<div class="alert alert-success">${res.message}</div>`;
            document.getElementById('dataForm').reset();
            loadData()
            setTimeout(() => {
              toggleDiv("viewer")
              hideLoader()
              status.innerHTML = ""
            }, 1000);
          } else {
            status.innerHTML = `<div class="alert alert-danger">${res.message}</div>`;
          }
        }).saveToSheet(formData);
      });

    function showCertificate(item) {
      document.getElementById('cer-name').textContent = item.fullname;
      document.getElementById('cer-date').textContent = item.timestamp;
      new bootstrap.Modal(document.getElementById('certificateModal')).show();
    }

    function printCertificate() {
      window.print();
    }
 
    function showLoader() {
      document.body.classList.add('no-scroll');
      document.getElementById("overlayLoader").style.display = "flex";
    }

    function hideLoader() {
      document.body.classList.remove('no-scroll');
      document.getElementById("overlayLoader").style.display = "none";
    }

    document.addEventListener('DOMContentLoaded', loadData);
    showLoader()
  </script>

</body>

</html>

form.html

<div class="container px-3">
  <div class="row justify-content-center">
    <div class="col-md-8">
      <div class="card p-3">
        <h3 class="mb-4">ลงนามถวายพระพรชัยมงคล</h3>

        <form id="dataForm">
          <div class="mb-3">
            <label class="form-label">ชื่อ นามสกุล</label>
            <input type="text" class="form-control" name="fullname" required>
          </div>
          <div class="mb-3">
            <label class="form-label">หน่วยงาน</label>
            <input type="text" class="form-control" name="unit" required>
          </div>
          <div class="mb-3">
            <label class="form-label">จังหวัด</label>
            <input type="text" class="form-control" name="province" required>
          </div>
          <div class="mb-3">
            <label class="form-label">ข้อความถวายพระพร</label>
            <select class="form-select" name="message"  required>
              <option value="" selected>เลือก...</option>
              <option value="ขอพระองค์ทรงพระเจริญ">ขอพระองค์ทรงพระเจริญ</option>
              <option value="ขอพระองค์ทรงพระเจริญยิ่งยืนนาน">ขอพระองค์ทรงพระเจริญยิ่งยืนนาน</option>
              <option value="ขอพระองค์ทรงมีพระพลานามัยที่สมบูรณ์แข็งแรง">ขอพระองค์ทรงมีพระพลานามัยที่สมบูรณ์แข็งแรง</option>
              <option value="ขออานุภาพแห่งคุณพระศรีรัตนตรัย โปรดดลบันดาลให้ทรงพระเกษมสำราญ">ขออานุภาพแห่งคุณพระศรีรัตนตรัย โปรดดลบันดาลให้ทรงพระเกษมสำราญ</option>
              <option value="ทรงพระเจริญ ด้วยเกล้าด้วยกระหม่อม ขอเดชะ">ทรงพระเจริญ ด้วยเกล้าด้วยกระหม่อม ขอเดชะ</option>
              <option value="ข้าพระพุทธเจ้า ขอถวายพระพรชัยมงคล ขอจงทรงพระสิริสวัสดิ์">ข้าพระพุทธเจ้า ขอถวายพระพรชัยมงคล ขอจงทรงพระสิริสวัสดิ์</option>
            </select>
          </div>
          <!-- <div class="mb-3">
            <label class="form-label">ข้อความ</label>
            <textarea class="form-control" name="message" rows="3" required></textarea>
          </div> -->
          <div class="text-center">
          <button type="submit" class="btn btn-primary">บันทึก</button>
          <a href="#" class="btn btn-success" onclick="toggleDiv('viewer'); event.preventDefault();">ดูข้อมูล</a>
          </div>
        </form>
      </div>
    </div>
  </div>
  <div id="status" class="mt-3"></div>
</div>


ดาวน์โหลด



แสดงความคิดเห็น (0)
ใหม่กว่า เก่ากว่า