Skip to content

Instantly share code, notes, and snippets.

@hectorgool
Last active June 18, 2025 18:50
Show Gist options
  • Save hectorgool/894b6d0086298943ed2963b2e5290e28 to your computer and use it in GitHub Desktop.
Save hectorgool/894b6d0086298943ed2963b2e5290e28 to your computer and use it in GitHub Desktop.
CRUD table, Business Management
"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