Created
January 4, 2026 17:29
-
-
Save do-me/bffbabf41fd47c0e4129534ce2fe2b37 to your computer and use it in GitHub Desktop.
supabase guest form
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en" class="bg-slate-50"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Guest List Manager</title> | |
| <!-- 1. Tailwind CSS via CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- 2. Supabase Client via CDN --> | |
| <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> | |
| </head> | |
| <body class="font-sans text-slate-800"> | |
| <div class="container mx-auto max-w-4xl p-4 sm:p-6 lg:p-8"> | |
| <!-- A. HEADER --> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-slate-900"> | |
| Guest List Manager | |
| </h1> | |
| <p class="text-slate-600 mt-1"> | |
| A complete interface to manage your guests. | |
| </p> | |
| </header> | |
| <main class="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
| <!-- B. FORM FOR CREATE & UPDATE --> | |
| <div class="md:col-span-1"> | |
| <form | |
| id="guest-form" | |
| class="bg-white p-6 rounded-lg shadow-sm" | |
| > | |
| <h2 id="form-title" class="text-2xl font-bold mb-4"> | |
| Add New Guest | |
| </h2> | |
| <input type="hidden" id="guest-id" /> | |
| <!-- Form Fields --> | |
| <div class="space-y-4"> | |
| <!-- Name & Family Name --> | |
| <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> | |
| <div> | |
| <label | |
| for="name" | |
| class="block text-sm font-medium text-slate-700" | |
| >First Name</label | |
| > | |
| <input | |
| type="text" | |
| id="name" | |
| name="name" | |
| required | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| placeholder="Caloggero" | |
| /> | |
| </div> | |
| <div> | |
| <label | |
| for="family_name" | |
| class="block text-sm font-medium text-slate-700" | |
| >Family Name</label | |
| > | |
| <input | |
| type="text" | |
| id="family_name" | |
| name="family_name" | |
| required | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| placeholder="Siciliano" | |
| /> | |
| </div> | |
| </div> | |
| <!-- Attending & Children --> | |
| <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> | |
| <div> | |
| <label | |
| for="attending" | |
| class="block text-sm font-medium text-slate-700" | |
| >Attending?</label | |
| > | |
| <select | |
| id="attending" | |
| name="attending" | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| > | |
| <option value="true">Yes</option> | |
| <option value="false">No</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label | |
| for="children" | |
| class="block text-sm font-medium text-slate-700" | |
| >Children</label | |
| > | |
| <input | |
| type="number" | |
| id="children" | |
| name="children" | |
| min="0" | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| placeholder="0" | |
| /> | |
| </div> | |
| </div> | |
| <!-- Allergies --> | |
| <div> | |
| <label | |
| for="allergies" | |
| class="block text-sm font-medium text-slate-700" | |
| >Allergies</label | |
| > | |
| <input | |
| type="text" | |
| id="allergies" | |
| name="allergies" | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| placeholder="Fish, nuts, etc." | |
| /> | |
| </div> | |
| <!-- Notes --> | |
| <div> | |
| <label | |
| for="notes" | |
| class="block text-sm font-medium text-slate-700" | |
| >Notes</label | |
| > | |
| <textarea | |
| id="notes" | |
| name="notes" | |
| rows="3" | |
| class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" | |
| placeholder="Special requests..." | |
| ></textarea> | |
| </div> | |
| </div> | |
| <!-- Form Action Buttons --> | |
| <div class="mt-6 flex items-center gap-4"> | |
| <button | |
| type="submit" | |
| class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" | |
| > | |
| Save Guest | |
| </button> | |
| <button | |
| type="button" | |
| id="cancel-edit-btn" | |
| class="hidden inline-flex justify-center rounded-md border border-slate-300 bg-white py-2 px-4 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- C. TABLE FOR READING & DELETING --> | |
| <div class="md:col-span-2"> | |
| <div class="bg-white overflow-hidden shadow-sm rounded-lg"> | |
| <div id="guest-list" class="divide-y divide-slate-200"> | |
| <!-- Guest rows will be injected here --> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- D. JAVASCRIPT LOGIC --> | |
| <script type="module"> | |
| // --- 1. SETUP --- | |
| const supabaseUrl = "https://erger....supabase.co"; | |
| const supabaseKey = | |
| "eyJhbGciOiJIUzI1NqdpbpQ..."; | |
| const _supabase = supabase.createClient(supabaseUrl, supabaseKey); | |
| // --- 2. DOM ELEMENT REFERENCES --- | |
| const guestList = document.getElementById("guest-list"); | |
| const guestForm = document.getElementById("guest-form"); | |
| const formTitle = document.getElementById("form-title"); | |
| const guestIdInput = document.getElementById("guest-id"); | |
| const cancelEditBtn = document.getElementById("cancel-edit-btn"); | |
| // --- 3. READ (Fetch and Display Guests) --- | |
| const fetchGuests = async () => { | |
| const { data: guests, error } = await _supabase | |
| .from("guests") | |
| .select("*") | |
| .order("created_at", { ascending: false }); | |
| if (error) { | |
| console.error("Error fetching guests:", error); | |
| return; | |
| } | |
| guestList.innerHTML = ""; | |
| if (guests.length === 0) { | |
| guestList.innerHTML = `<div class="p-4 text-center text-slate-500">No guests found. Add one!</div>`; | |
| } else { | |
| for (const guest of guests) { | |
| const guestEl = document.createElement("div"); | |
| guestEl.id = `guest-${guest.id}`; | |
| guestEl.className = | |
| "p-4 flex flex-col sm:flex-row justify-between sm:items-center gap-2"; | |
| guestEl.innerHTML = ` | |
| <div class="flex-1"> | |
| <p class="font-semibold text-slate-800">${guest.name} ${guest.family_name}</p> | |
| <p class="text-sm text-slate-600"> | |
| <span class="font-medium">Attending:</span> ${guest.attending ? "Yes" : "No"} | | |
| <span class="font-medium">Children:</span> ${guest.children ?? 0} | |
| ${guest.allergies ? `| <span class="font-medium">Allergies:</span> ${guest.allergies}` : ""} | |
| </p> | |
| ${guest.notes ? `<p class="mt-1 text-xs text-slate-500 bg-slate-100 p-2 rounded-md"><span class="font-medium">Notes:</span> ${guest.notes}</p>` : ""} | |
| </div> | |
| <div class="flex gap-2 self-end sm:self-center"> | |
| <button data-id="${guest.id}" class="edit-btn text-sm font-medium text-blue-600 hover:text-blue-800">Edit</button> | |
| <button data-id="${guest.id}" class="delete-btn text-sm font-medium text-red-600 hover:text-red-800">Delete</button> | |
| </div> | |
| `; | |
| guestList.appendChild(guestEl); | |
| } | |
| } | |
| }; | |
| // --- 4. CREATE and UPDATE --- | |
| const handleFormSubmit = async (event) => { | |
| event.preventDefault(); | |
| const form = event.target; | |
| const formData = new FormData(form); | |
| const guestData = { | |
| name: formData.get("name"), | |
| family_name: formData.get("family_name"), | |
| attending: formData.get("attending") === "true", | |
| children: parseInt(formData.get("children")) || null, // Parse to int, default to null | |
| allergies: formData.get("allergies") || null, | |
| notes: formData.get("notes") || null, | |
| }; | |
| const id = guestIdInput.value; | |
| let error; | |
| if (id) { | |
| // UPDATE | |
| const { error: updateError } = await _supabase | |
| .from("guests") | |
| .update(guestData) | |
| .eq("id", id); | |
| error = updateError; | |
| } else { | |
| // CREATE | |
| const { error: insertError } = await _supabase | |
| .from("guests") | |
| .insert([guestData]); | |
| error = insertError; | |
| } | |
| if (error) { | |
| alert("Error: " + error.message); | |
| } else { | |
| alert(id ? "Guest updated!" : "Guest added!"); | |
| resetForm(); | |
| await fetchGuests(); | |
| } | |
| }; | |
| // --- 5. DELETE --- | |
| const handleDeleteGuest = async (id) => { | |
| if (!confirm("Are you sure?")) return; | |
| const { error } = await _supabase | |
| .from("guests") | |
| .delete() | |
| .eq("id", id); | |
| if (error) alert("Error: " + error.message); | |
| else { | |
| alert("Guest deleted!"); | |
| document.getElementById(`guest-${id}`).remove(); | |
| } | |
| }; | |
| // --- 6. Form State Management (for editing) --- | |
| const prepareFormForEdit = (guest) => { | |
| formTitle.textContent = "Edit Guest"; | |
| guestIdInput.value = guest.id; | |
| guestForm.elements["name"].value = guest.name; | |
| guestForm.elements["family_name"].value = guest.family_name; | |
| guestForm.elements["attending"].value = guest.attending; | |
| guestForm.elements["children"].value = guest.children || ""; | |
| guestForm.elements["allergies"].value = guest.allergies || ""; | |
| guestForm.elements["notes"].value = guest.notes || ""; | |
| cancelEditBtn.classList.remove("hidden"); | |
| window.scrollTo({ top: 0, behavior: "smooth" }); | |
| }; | |
| const resetForm = () => { | |
| formTitle.textContent = "Add New Guest"; | |
| guestForm.reset(); | |
| guestIdInput.value = ""; | |
| cancelEditBtn.classList.add("hidden"); | |
| }; | |
| // --- 7. EVENT LISTENERS --- | |
| guestForm.addEventListener("submit", handleFormSubmit); | |
| cancelEditBtn.addEventListener("click", resetForm); | |
| guestList.addEventListener("click", async (event) => { | |
| const target = event.target; | |
| const id = target.dataset.id; | |
| if (!id) return; | |
| if (target.classList.contains("delete-btn")) | |
| await handleDeleteGuest(id); | |
| if (target.classList.contains("edit-btn")) { | |
| const { data: guest, error } = await _supabase | |
| .from("guests") | |
| .select("*") | |
| .eq("id", id) | |
| .single(); | |
| if (error) | |
| alert( | |
| "Could not fetch guest details: " + error.message, | |
| ); | |
| else prepareFormForEdit(guest); | |
| } | |
| }); | |
| // --- 8. INITIAL LOAD --- | |
| fetchGuests(); | |
| </script> | |
| </body> | |
| </html> |
Author
do-me
commented
Jan 4, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment