Last active
June 18, 2025 18:50
-
-
Save hectorgool/894b6d0086298943ed2963b2e5290e28 to your computer and use it in GitHub Desktop.
CRUD table, Business Management
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
| "use client"; | |
| import { useEffect, useState, FormEvent } from "react"; | |
| import { createClient } from "@/lib/supabase/client"; | |
| import toast, { Toaster } from "react-hot-toast"; | |
| import Swal from "sweetalert2"; | |
| import "sweetalert2/dist/sweetalert2.min.css"; | |
| import { useRouter } from "next/navigation"; | |
| interface Business { | |
| business_id: number; | |
| business_name: string; | |
| slogan: string; | |
| business_description?: string; | |
| user_id: string; | |
| is_active?: boolean; | |
| created_at?: string; | |
| updated_at?: string; | |
| } | |
| type BusinessForm = { | |
| business_name: string; | |
| slogan: string; | |
| business_description?: string; | |
| }; | |
| export default function Businesses() { | |
| const [businesses, setBusinesses] = useState<Business[]>([]); | |
| const [form, setForm] = useState<BusinessForm>({ | |
| business_name: "", | |
| slogan: "", | |
| business_description: "", | |
| }); | |
| const [editId, setEditId] = useState<number | null>(null); | |
| const [userId, setUserId] = useState<string | null>(null); | |
| const [loading, setLoading] = useState<boolean>(false); | |
| const [validated, setValidated] = useState<boolean>(false); | |
| const [currentPage, setCurrentPage] = useState<number>(1); | |
| const perPage = 5; | |
| const [totalCount, setTotalCount] = useState<number>(0); | |
| const router = useRouter(); | |
| const supabase = createClient(); | |
| useEffect(() => { | |
| const checkUser = async () => { | |
| setLoading(true); | |
| const { data, error } = await supabase.auth.getUser(); | |
| if (error || !data?.user) { | |
| router.push("/auth/login"); | |
| } else { | |
| setUserId(data.user.id); | |
| await fetchBusinesses(data.user.id, currentPage); | |
| } | |
| setLoading(false); | |
| }; | |
| checkUser(); | |
| }, []); | |
| async function handlePageChange(page: number) { | |
| if (userId) { | |
| setCurrentPage(page); | |
| await fetchBusinesses(userId, page); | |
| } | |
| } | |
| async function fetchBusinesses(user_id: string, page: number) { | |
| setLoading(true); | |
| const from = (page - 1) * perPage; | |
| const to = from + perPage - 1; | |
| const { data, error, count } = await supabase | |
| .from("businesses") | |
| .select("*", { count: "exact" }) | |
| .eq("user_id", user_id) | |
| .range(from, to); | |
| if (error) { | |
| toast.error(`Failed to read data: ${error.message}`); | |
| } else { | |
| setBusinesses(data || []); | |
| if (typeof count === "number") setTotalCount(count); | |
| } | |
| setLoading(false); | |
| } | |
| async function handleFormSubmit(event: FormEvent<HTMLFormElement>) { | |
| event.preventDefault(); | |
| const formEl = event.currentTarget; | |
| setValidated(true); | |
| if (!formEl.checkValidity()) { | |
| return; | |
| } | |
| if (!userId) { | |
| toast.error("Usuario no autenticado"); | |
| return; | |
| } | |
| setLoading(true); | |
| if (editId !== null) { | |
| const { error } = await supabase | |
| .from("businesses") | |
| .update({ ...form }) | |
| .eq("business_id", editId); | |
| if (error) { | |
| toast.error("Failed to update business"); | |
| } else { | |
| toast.success("Business updated successfully"); | |
| setEditId(null); | |
| setForm({ business_name: "", slogan: "", business_description: "" }); | |
| setValidated(false); | |
| } | |
| } else { | |
| const { error } = await supabase | |
| .from("businesses") | |
| .insert([{ ...form, user_id: userId }]); | |
| if (error) { | |
| toast.error(`Failed to create: ${error.message}`); | |
| } else { | |
| toast.success("Business added successfully"); | |
| setForm({ business_name: "", slogan: "", business_description: "" }); | |
| setValidated(false); | |
| } | |
| } | |
| await fetchBusinesses(userId, currentPage); | |
| setLoading(false); | |
| } | |
| function handleBusinessEdit(business: Business) { | |
| setForm({ | |
| business_name: business.business_name, | |
| slogan: business.slogan, | |
| business_description: business.business_description || "", | |
| }); | |
| setEditId(business.business_id); | |
| setValidated(false); | |
| } | |
| async function handleBusinessDelete(id: number) { | |
| const result = await Swal.fire({ | |
| title: "Are you sure?", | |
| text: "You won't be able to revert this!", | |
| icon: "warning", | |
| showCancelButton: true, | |
| confirmButtonColor: "#343a40", | |
| cancelButtonColor: "#49080b", | |
| confirmButtonText: "Yes, delete it!", | |
| }); | |
| if (result.isConfirmed) { | |
| setLoading(true); | |
| const { error } = await supabase | |
| .from("businesses") | |
| .delete() | |
| .eq("business_id", id); | |
| if (error) { | |
| toast.error("Failed to delete business"); | |
| } else { | |
| toast.success("Business deleted successfully"); | |
| if (userId) { | |
| const { count } = await supabase | |
| .from("businesses") | |
| .select("*", { count: "exact", head: true }) | |
| .eq("user_id", userId); | |
| const newTotalCount = count ?? 0; | |
| const newTotalPages = Math.ceil(newTotalCount / perPage); | |
| const newPage = | |
| currentPage > newTotalPages ? newTotalPages : currentPage; | |
| setCurrentPage(newPage); | |
| await fetchBusinesses(userId, newPage); | |
| setTotalCount(newTotalCount); | |
| } | |
| } | |
| setLoading(false); | |
| } | |
| } | |
| const totalPages = Math.ceil(totalCount / perPage); | |
| const counterStart = (currentPage - 1) * perPage + 1; | |
| return ( | |
| <div className="container my-5"> | |
| <Toaster /> | |
| <h3 className="mb-4">Business Management</h3> | |
| <div className="row"> | |
| <div className="col-md-4"> | |
| <div className="card mb-4"> | |
| <div className="card-body"> | |
| <form | |
| noValidate | |
| onSubmit={handleFormSubmit} | |
| className={`needs-validation ${validated ? "was-validated" : ""}`} | |
| > | |
| <div className="mb-3"> | |
| <label className="form-label">Name *</label> | |
| <input | |
| type="text" | |
| className="form-control" | |
| value={form.business_name} | |
| onChange={(e) => | |
| setForm({ ...form, business_name: e.target.value }) | |
| } | |
| required | |
| minLength={2} | |
| /> | |
| <div className="invalid-feedback"> | |
| Please enter a valid business name (at least 2 characters). | |
| </div> | |
| <div className="valid-feedback">Looks good!</div> | |
| </div> | |
| <div className="mb-3"> | |
| <label className="form-label">Slogan</label> | |
| <input | |
| type="text" | |
| className="form-control" | |
| value={form.slogan} | |
| onChange={(e) => | |
| setForm({ ...form, slogan: e.target.value }) | |
| } | |
| minLength={2} | |
| /> | |
| <div className="invalid-feedback"> | |
| Slogan should be at least 2 characters if provided. | |
| </div> | |
| <div className="valid-feedback">Looks good!</div> | |
| </div> | |
| <div className="mb-3"> | |
| <label className="form-label">Description</label> | |
| <textarea | |
| className="form-control" | |
| value={form.business_description} | |
| onChange={(e) => | |
| setForm({ ...form, business_description: e.target.value }) | |
| } | |
| rows={3} | |
| /> | |
| <div className="valid-feedback">Looks good!</div> | |
| </div> | |
| <button className="btn btn-success w-100 btn-lg"> | |
| {editId !== null ? "Edit" : "Add"} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="col-md-8"> | |
| {loading ? ( | |
| <div className="text-center my-5"> | |
| <div className="spinner-border text-primary" role="status"> | |
| <span className="visually-hidden">Loading...</span> | |
| </div> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="table-responsive"> | |
| <table className="table table-bordered"> | |
| <thead className="table-light"> | |
| <tr> | |
| <th className="text-center">ID</th> | |
| <th>Name</th> | |
| <th>Slogan</th> | |
| {/* <th>Description</th> */} | |
| <th className="text-center">Edit</th> | |
| <th className="text-center">Delete</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {businesses.map((business, index) => ( | |
| <tr key={business.business_id}> | |
| <td className="align-middle text-end"> | |
| {counterStart + index} | |
| </td> | |
| <td className="align-middle"> | |
| {business.business_name} | |
| </td> | |
| <td className="align-middle">{business.slogan}</td> | |
| {/* | |
| <td className="align-middle"> | |
| {business.business_description} | |
| </td> | |
| */} | |
| <td className="text-center"> | |
| <button | |
| className="btn btn-success btn-lg" | |
| onClick={() => handleBusinessEdit(business)} | |
| > | |
| <i className="bi bi-pencil"></i> | |
| </button> | |
| </td> | |
| <td className="text-center"> | |
| <button | |
| className="btn btn-danger btn-lg" | |
| onClick={() => | |
| handleBusinessDelete(business.business_id) | |
| } | |
| > | |
| <i className="bi bi-trash"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| ))} | |
| {businesses.length === 0 && ( | |
| <tr> | |
| <td colSpan={6} className="text-center text-muted"> | |
| No businesses found. | |
| </td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| {totalPages > 1 && ( | |
| <div className="d-flex justify-content-center align-items-center mt-4 gap-2"> | |
| <button | |
| className="btn btn-success btn-lg" | |
| disabled={currentPage === 1} | |
| onClick={() => handlePageChange(currentPage - 1)} | |
| > | |
| Prev | |
| </button> | |
| <span> | |
| Page {currentPage} of {totalPages} | |
| </span> | |
| <button | |
| className="btn btn-success btn-lg" | |
| disabled={currentPage === totalPages} | |
| onClick={() => handlePageChange(currentPage + 1)} | |
| > | |
| Next | |
| </button> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment