mirror of
https://github.com/skidoodle/budgetable.git
synced 2026-04-28 15:57:36 +02:00
feat: enhance ui components and response handling
This commit is contained in:
+111
-129
@@ -1,150 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import TableWrapper from "@/components/table-wrapper";
|
||||
import Header from "@/components/header";
|
||||
import TotalDisplay from "@/components/total-display";
|
||||
import { toast } from "sonner";
|
||||
import { type Budgetable, areRowsEqual } from "@/lib/utils";
|
||||
|
||||
const DEFAULT_NEW_ROW: Budgetable = {
|
||||
id: "",
|
||||
title: "",
|
||||
price: 0,
|
||||
link: "",
|
||||
note: "",
|
||||
status: "Unpaid",
|
||||
};
|
||||
|
||||
const ENDPOINT = "/pocketbase";
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<Budgetable[]>(() => []);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newRow, setNewRow] = useState<Budgetable>({
|
||||
id: "",
|
||||
title: "",
|
||||
price: 0,
|
||||
link: "",
|
||||
note: "",
|
||||
status: "Unpaid",
|
||||
});
|
||||
const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [data, setData] = useState<Budgetable[]>([]);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newRow, setNewRow] = useState<Budgetable>(DEFAULT_NEW_ROW);
|
||||
const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/pocketbase");
|
||||
if (!res.ok) throw new Error("Failed to fetch data");
|
||||
const records: Budgetable[] = await res.json();
|
||||
setData(records);
|
||||
} catch (err) {
|
||||
toast.error("Error fetching data. Please try again later.");
|
||||
console.error("Error fetching data:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(ENDPOINT);
|
||||
if (!res.ok) throw new Error("Failed to fetch data");
|
||||
const records: Budgetable[] = await res.json();
|
||||
setData(records);
|
||||
} catch (err) {
|
||||
toast.error("Error fetching data. Please try again later.");
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleSave = async (
|
||||
updatedRow: Budgetable,
|
||||
originalRow: Budgetable,
|
||||
) => {
|
||||
if (areRowsEqual(updatedRow, originalRow)) {
|
||||
return;
|
||||
}
|
||||
const handleSave = useCallback(async (updatedRow: Budgetable, originalRow: Budgetable) => {
|
||||
if (areRowsEqual(updatedRow, originalRow)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pocketbase/${updatedRow.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedRow),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to update row");
|
||||
const updatedData = await res.json();
|
||||
setData((prev) =>
|
||||
prev.map((row) => (row.id === updatedRow.id ? updatedData : row)),
|
||||
);
|
||||
try {
|
||||
const res = await fetch(`${ENDPOINT}/${updatedRow.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedRow),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to update row");
|
||||
|
||||
setRecentlyUpdatedRowId(updatedRow.id);
|
||||
setTimeout(() => setRecentlyUpdatedRowId(null), 500);
|
||||
toast.success("Row updated successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error updating row. Please try again.");
|
||||
console.error("Error updating row:", err);
|
||||
}
|
||||
};
|
||||
const updatedData = await res.json();
|
||||
setData((prev) => prev.map((row) => (row.id === updatedRow.id ? updatedData : row)));
|
||||
|
||||
const handleAddRow = async () => {
|
||||
if (!newRow.title || newRow.price <= 0) {
|
||||
toast("Title and price are required.");
|
||||
return;
|
||||
}
|
||||
setRecentlyUpdatedRowId(updatedRow.id);
|
||||
setTimeout(() => setRecentlyUpdatedRowId(null), 500);
|
||||
toast.success("Row updated successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error updating row. Please try again.");
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
try {
|
||||
const res = await fetch("/pocketbase", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(newRow),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to add row");
|
||||
const record: Budgetable = await res.json();
|
||||
setData((prev) => [...prev, record]);
|
||||
setNewRow({
|
||||
id: "",
|
||||
title: "",
|
||||
price: 0,
|
||||
link: "",
|
||||
note: "",
|
||||
status: "Unpaid",
|
||||
});
|
||||
toast.success("Row added successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error adding row. Please try again.");
|
||||
console.error("Error adding row:", err);
|
||||
}
|
||||
};
|
||||
const handleAddRow = useCallback(async () => {
|
||||
if (!newRow.title || newRow.price <= 0) {
|
||||
toast.error("Title and price are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDeleteRow = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`/pocketbase/${id}`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error("Failed to delete row");
|
||||
setData((prev) => prev.filter((row) => row.id !== id));
|
||||
toast.success("Row deleted successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error deleting row. Please try again.");
|
||||
console.error("Error deleting row:", err);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const res = await fetch(ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(newRow),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to add row");
|
||||
|
||||
const toggleStatus = async (row: Budgetable) => {
|
||||
const updatedStatus: "Paid" | "Unpaid" =
|
||||
row.status === "Paid" ? "Unpaid" : "Paid";
|
||||
const updatedRow: Budgetable = { ...row, status: updatedStatus };
|
||||
await handleSave(updatedRow, row);
|
||||
setData((prev) =>
|
||||
prev.map((item) => (item.id === row.id ? updatedRow : item)),
|
||||
);
|
||||
};
|
||||
const record: Budgetable = await res.json();
|
||||
setData((prev) => [...prev, record]);
|
||||
setNewRow(DEFAULT_NEW_ROW);
|
||||
toast.success("Row added successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error adding row. Please try again.");
|
||||
console.error(err);
|
||||
}
|
||||
}, [newRow]);
|
||||
|
||||
const total = data.reduce(
|
||||
(sum, item) => sum + (item.status === "Unpaid" ? item.price : 0),
|
||||
0,
|
||||
);
|
||||
const handleDeleteRow = useCallback(async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error("Failed to delete row");
|
||||
|
||||
return (
|
||||
<main className="container mx-auto p-4 max-w-5xl">
|
||||
{loading}
|
||||
<Header isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<TotalDisplay total={total} />
|
||||
<TableWrapper
|
||||
data={data}
|
||||
isEditing={isEditing}
|
||||
setData={setData}
|
||||
newRow={newRow}
|
||||
setNewRow={setNewRow}
|
||||
recentlyUpdatedRowId={recentlyUpdatedRowId}
|
||||
handleSave={handleSave}
|
||||
handleAddRow={handleAddRow}
|
||||
handleDeleteRow={handleDeleteRow}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
setData((prev) => prev.filter((row) => row.id !== id));
|
||||
toast.success("Row deleted successfully!");
|
||||
} catch (err) {
|
||||
toast.error("Error deleting row. Please try again.");
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleStatus = useCallback(async (row: Budgetable) => {
|
||||
const updatedStatus = row.status === "Paid" ? "Unpaid" : "Paid";
|
||||
const updatedRow: Budgetable = { ...row, status: updatedStatus as "Paid" | "Unpaid" };
|
||||
await handleSave(updatedRow, row);
|
||||
}, [handleSave]);
|
||||
|
||||
const total = data.reduce(
|
||||
(sum, item) => sum + (item.status === "Unpaid" ? item.price : 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container mx-auto p-4 max-w-7xl">
|
||||
<Header isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<TotalDisplay total={total} />
|
||||
<TableWrapper
|
||||
data={data}
|
||||
isEditing={isEditing}
|
||||
setData={setData}
|
||||
newRow={newRow}
|
||||
setNewRow={setNewRow}
|
||||
recentlyUpdatedRowId={recentlyUpdatedRowId}
|
||||
handleSave={handleSave}
|
||||
handleAddRow={handleAddRow}
|
||||
handleDeleteRow={handleDeleteRow}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user