feat: enhance ui components and response handling

This commit is contained in:
2025-01-03 18:48:57 +01:00
parent 78fbcebe91
commit 83b3a747a1
11 changed files with 296 additions and 328 deletions
+111 -129
View File
@@ -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>
);
}