|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useState, useEffect } from "react"; |
| 3 | +import { useState, useEffect, useCallback } from "react"; |
4 | 4 | import TableWrapper from "@/components/table-wrapper"; |
5 | 5 | import Header from "@/components/header"; |
6 | 6 | import TotalDisplay from "@/components/total-display"; |
7 | 7 | import { toast } from "sonner"; |
8 | 8 | import { type Budgetable, areRowsEqual } from "@/lib/utils"; |
9 | 9 |
|
| 10 | +const DEFAULT_NEW_ROW: Budgetable = { |
| 11 | + id: "", |
| 12 | + title: "", |
| 13 | + price: 0, |
| 14 | + link: "", |
| 15 | + note: "", |
| 16 | + status: "Unpaid", |
| 17 | +}; |
| 18 | + |
| 19 | +const ENDPOINT = "/pocketbase"; |
| 20 | + |
10 | 21 | export default function App() { |
11 | | - const [data, setData] = useState<Budgetable[]>(() => []); |
12 | | - const [isEditing, setIsEditing] = useState(false); |
13 | | - const [loading, setLoading] = useState(false); |
14 | | - const [newRow, setNewRow] = useState<Budgetable>({ |
15 | | - id: "", |
16 | | - title: "", |
17 | | - price: 0, |
18 | | - link: "", |
19 | | - note: "", |
20 | | - status: "Unpaid", |
21 | | - }); |
22 | | - const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState< |
23 | | - string | null |
24 | | - >(null); |
25 | | - |
26 | | - useEffect(() => { |
27 | | - async function fetchData() { |
28 | | - setLoading(true); |
29 | | - try { |
30 | | - const res = await fetch("/pocketbase"); |
31 | | - if (!res.ok) throw new Error("Failed to fetch data"); |
32 | | - const records: Budgetable[] = await res.json(); |
33 | | - setData(records); |
34 | | - } catch (err) { |
35 | | - toast.error("Error fetching data. Please try again later."); |
36 | | - console.error("Error fetching data:", err); |
37 | | - } finally { |
38 | | - setLoading(false); |
39 | | - } |
40 | | - } |
41 | | - |
42 | | - fetchData(); |
43 | | - }, []); |
44 | | - |
45 | | - const handleSave = async ( |
46 | | - updatedRow: Budgetable, |
47 | | - originalRow: Budgetable, |
48 | | - ) => { |
49 | | - if (areRowsEqual(updatedRow, originalRow)) { |
50 | | - return; |
51 | | - } |
52 | | - |
53 | | - try { |
54 | | - const res = await fetch(`/pocketbase/${updatedRow.id}`, { |
55 | | - method: "PUT", |
56 | | - headers: { "Content-Type": "application/json" }, |
57 | | - body: JSON.stringify(updatedRow), |
58 | | - }); |
59 | | - if (!res.ok) throw new Error("Failed to update row"); |
60 | | - const updatedData = await res.json(); |
61 | | - setData((prev) => |
62 | | - prev.map((row) => (row.id === updatedRow.id ? updatedData : row)), |
63 | | - ); |
64 | | - |
65 | | - setRecentlyUpdatedRowId(updatedRow.id); |
66 | | - setTimeout(() => setRecentlyUpdatedRowId(null), 500); |
67 | | - toast.success("Row updated successfully!"); |
68 | | - } catch (err) { |
69 | | - toast.error("Error updating row. Please try again."); |
70 | | - console.error("Error updating row:", err); |
71 | | - } |
72 | | - }; |
73 | | - |
74 | | - const handleAddRow = async () => { |
75 | | - if (!newRow.title || newRow.price <= 0) { |
76 | | - toast("Title and price are required."); |
77 | | - return; |
78 | | - } |
79 | | - |
80 | | - try { |
81 | | - const res = await fetch("/pocketbase", { |
82 | | - method: "POST", |
83 | | - headers: { "Content-Type": "application/json" }, |
84 | | - body: JSON.stringify(newRow), |
85 | | - }); |
86 | | - if (!res.ok) throw new Error("Failed to add row"); |
87 | | - const record: Budgetable = await res.json(); |
88 | | - setData((prev) => [...prev, record]); |
89 | | - setNewRow({ |
90 | | - id: "", |
91 | | - title: "", |
92 | | - price: 0, |
93 | | - link: "", |
94 | | - note: "", |
95 | | - status: "Unpaid", |
96 | | - }); |
97 | | - toast.success("Row added successfully!"); |
98 | | - } catch (err) { |
99 | | - toast.error("Error adding row. Please try again."); |
100 | | - console.error("Error adding row:", err); |
101 | | - } |
102 | | - }; |
103 | | - |
104 | | - const handleDeleteRow = async (id: string) => { |
105 | | - try { |
106 | | - const res = await fetch(`/pocketbase/${id}`, { method: "DELETE" }); |
107 | | - if (!res.ok) throw new Error("Failed to delete row"); |
108 | | - setData((prev) => prev.filter((row) => row.id !== id)); |
109 | | - toast.success("Row deleted successfully!"); |
110 | | - } catch (err) { |
111 | | - toast.error("Error deleting row. Please try again."); |
112 | | - console.error("Error deleting row:", err); |
113 | | - } |
114 | | - }; |
115 | | - |
116 | | - const toggleStatus = async (row: Budgetable) => { |
117 | | - const updatedStatus: "Paid" | "Unpaid" = |
118 | | - row.status === "Paid" ? "Unpaid" : "Paid"; |
119 | | - const updatedRow: Budgetable = { ...row, status: updatedStatus }; |
120 | | - await handleSave(updatedRow, row); |
121 | | - setData((prev) => |
122 | | - prev.map((item) => (item.id === row.id ? updatedRow : item)), |
123 | | - ); |
124 | | - }; |
125 | | - |
126 | | - const total = data.reduce( |
127 | | - (sum, item) => sum + (item.status === "Unpaid" ? item.price : 0), |
128 | | - 0, |
129 | | - ); |
130 | | - |
131 | | - return ( |
132 | | - <main className="container mx-auto p-4 max-w-5xl"> |
133 | | - {loading} |
134 | | - <Header isEditing={isEditing} setIsEditing={setIsEditing} /> |
135 | | - <TotalDisplay total={total} /> |
136 | | - <TableWrapper |
137 | | - data={data} |
138 | | - isEditing={isEditing} |
139 | | - setData={setData} |
140 | | - newRow={newRow} |
141 | | - setNewRow={setNewRow} |
142 | | - recentlyUpdatedRowId={recentlyUpdatedRowId} |
143 | | - handleSave={handleSave} |
144 | | - handleAddRow={handleAddRow} |
145 | | - handleDeleteRow={handleDeleteRow} |
146 | | - toggleStatus={toggleStatus} |
147 | | - /> |
148 | | - </main> |
149 | | - ); |
| 22 | + const [data, setData] = useState<Budgetable[]>([]); |
| 23 | + const [isEditing, setIsEditing] = useState(false); |
| 24 | + const [newRow, setNewRow] = useState<Budgetable>(DEFAULT_NEW_ROW); |
| 25 | + const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<string | null>(null); |
| 26 | + |
| 27 | + const fetchData = useCallback(async () => { |
| 28 | + try { |
| 29 | + const res = await fetch(ENDPOINT); |
| 30 | + if (!res.ok) throw new Error("Failed to fetch data"); |
| 31 | + const records: Budgetable[] = await res.json(); |
| 32 | + setData(records); |
| 33 | + } catch (err) { |
| 34 | + toast.error("Error fetching data. Please try again later."); |
| 35 | + console.error(err); |
| 36 | + } |
| 37 | + }, []); |
| 38 | + |
| 39 | + useEffect(() => { |
| 40 | + fetchData(); |
| 41 | + }, [fetchData]); |
| 42 | + |
| 43 | + const handleSave = useCallback(async (updatedRow: Budgetable, originalRow: Budgetable) => { |
| 44 | + if (areRowsEqual(updatedRow, originalRow)) return; |
| 45 | + |
| 46 | + try { |
| 47 | + const res = await fetch(`${ENDPOINT}/${updatedRow.id}`, { |
| 48 | + method: "PUT", |
| 49 | + headers: { "Content-Type": "application/json" }, |
| 50 | + body: JSON.stringify(updatedRow), |
| 51 | + }); |
| 52 | + if (!res.ok) throw new Error("Failed to update row"); |
| 53 | + |
| 54 | + const updatedData = await res.json(); |
| 55 | + setData((prev) => prev.map((row) => (row.id === updatedRow.id ? updatedData : row))); |
| 56 | + |
| 57 | + setRecentlyUpdatedRowId(updatedRow.id); |
| 58 | + setTimeout(() => setRecentlyUpdatedRowId(null), 500); |
| 59 | + toast.success("Row updated successfully!"); |
| 60 | + } catch (err) { |
| 61 | + toast.error("Error updating row. Please try again."); |
| 62 | + console.error(err); |
| 63 | + } |
| 64 | + }, []); |
| 65 | + |
| 66 | + const handleAddRow = useCallback(async () => { |
| 67 | + if (!newRow.title || newRow.price <= 0) { |
| 68 | + toast.error("Title and price are required."); |
| 69 | + return; |
| 70 | + } |
| 71 | + |
| 72 | + try { |
| 73 | + const res = await fetch(ENDPOINT, { |
| 74 | + method: "POST", |
| 75 | + headers: { "Content-Type": "application/json" }, |
| 76 | + body: JSON.stringify(newRow), |
| 77 | + }); |
| 78 | + if (!res.ok) throw new Error("Failed to add row"); |
| 79 | + |
| 80 | + const record: Budgetable = await res.json(); |
| 81 | + setData((prev) => [...prev, record]); |
| 82 | + setNewRow(DEFAULT_NEW_ROW); |
| 83 | + toast.success("Row added successfully!"); |
| 84 | + } catch (err) { |
| 85 | + toast.error("Error adding row. Please try again."); |
| 86 | + console.error(err); |
| 87 | + } |
| 88 | + }, [newRow]); |
| 89 | + |
| 90 | + const handleDeleteRow = useCallback(async (id: string) => { |
| 91 | + try { |
| 92 | + const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" }); |
| 93 | + if (!res.ok) throw new Error("Failed to delete row"); |
| 94 | + |
| 95 | + setData((prev) => prev.filter((row) => row.id !== id)); |
| 96 | + toast.success("Row deleted successfully!"); |
| 97 | + } catch (err) { |
| 98 | + toast.error("Error deleting row. Please try again."); |
| 99 | + console.error(err); |
| 100 | + } |
| 101 | + }, []); |
| 102 | + |
| 103 | + const toggleStatus = useCallback(async (row: Budgetable) => { |
| 104 | + const updatedStatus = row.status === "Paid" ? "Unpaid" : "Paid"; |
| 105 | + const updatedRow: Budgetable = { ...row, status: updatedStatus as "Paid" | "Unpaid" }; |
| 106 | + await handleSave(updatedRow, row); |
| 107 | + }, [handleSave]); |
| 108 | + |
| 109 | + const total = data.reduce( |
| 110 | + (sum, item) => sum + (item.status === "Unpaid" ? item.price : 0), |
| 111 | + 0, |
| 112 | + ); |
| 113 | + |
| 114 | + return ( |
| 115 | + <main className="container mx-auto p-4 max-w-7xl"> |
| 116 | + <Header isEditing={isEditing} setIsEditing={setIsEditing} /> |
| 117 | + <TotalDisplay total={total} /> |
| 118 | + <TableWrapper |
| 119 | + data={data} |
| 120 | + isEditing={isEditing} |
| 121 | + setData={setData} |
| 122 | + newRow={newRow} |
| 123 | + setNewRow={setNewRow} |
| 124 | + recentlyUpdatedRowId={recentlyUpdatedRowId} |
| 125 | + handleSave={handleSave} |
| 126 | + handleAddRow={handleAddRow} |
| 127 | + handleDeleteRow={handleDeleteRow} |
| 128 | + toggleStatus={toggleStatus} |
| 129 | + /> |
| 130 | + </main> |
| 131 | + ); |
150 | 132 | } |
0 commit comments