diff --git a/src/app/page.tsx b/src/app/page.tsx index c12afac..c867c5a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,125 +8,138 @@ import { toast } from "sonner"; import { type Budgetable, areRowsEqual } from "@/lib/utils"; const DEFAULT_NEW_ROW: Budgetable = { - id: "", - title: "", - price: 0, - link: "", - note: "", - status: "Unpaid", + id: "", + title: "", + price: 0, + link: "", + note: "", + status: "Unpaid", }; const ENDPOINT = "/pocketbase"; export default function App() { - const [data, setData] = useState([]); - const [isEditing, setIsEditing] = useState(false); - const [newRow, setNewRow] = useState(DEFAULT_NEW_ROW); - const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState(null); + const [data, setData] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const [newRow, setNewRow] = useState(DEFAULT_NEW_ROW); + const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState< + string | null + >(null); - 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); - } - }, []); + 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); + } + }, []); - useEffect(() => { - fetchData(); - }, [fetchData]); + useEffect(() => { + fetchData(); + }, [fetchData]); - const handleSave = useCallback(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(`${ENDPOINT}/${updatedRow.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(updatedRow), - }); - if (!res.ok) throw new Error("Failed to update 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"); - const updatedData = await res.json(); - setData((prev) => prev.map((row) => (row.id === updatedRow.id ? updatedData : row))); + const updatedData = await res.json(); + setData((prev) => + prev.map((row) => (row.id === updatedRow.id ? updatedData : 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(err); - } - }, []); + 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); + } + }, + [], + ); - const handleAddRow = useCallback(async () => { - if (!newRow.title || newRow.price <= 0) { - toast.error("Title and price are required."); - return; - } + const handleAddRow = useCallback(async () => { + if (!newRow.title || newRow.price <= 0) { + toast.error("Title and price are required."); + return; + } - 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"); + 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 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 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 handleDeleteRow = useCallback(async (id: string) => { - try { - const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" }); - if (!res.ok) throw new Error("Failed to delete row"); + 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"); - 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); - } - }, []); + 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 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, - ); + const total = data.reduce( + (sum, item) => sum + (item.status === "Unpaid" ? item.price : 0), + 0, + ); - return ( -
-
- - -
- ); + return ( +
+
+ + +
+ ); } diff --git a/src/app/pocketbase/[id]/route.ts b/src/app/pocketbase/[id]/route.ts index cbbe121..95a6c60 100644 --- a/src/app/pocketbase/[id]/route.ts +++ b/src/app/pocketbase/[id]/route.ts @@ -1,84 +1,94 @@ import pb from "@/lib/pocketbase"; import { ResponseHelper } from "@/lib/helper"; import { RESPONSE } from "@/lib/const"; -import { Budgetable } from "@/lib/utils"; +import type { Budgetable } from "@/lib/utils"; -const { INTERNAL_SERVER_ERROR, MISSING_ID, FAILED_TO_DELETE_DATA, FAILED_TO_UPDATE_DATA, INVALID_DATA, SUCCESS, CREATED } = RESPONSE; +const { + INTERNAL_SERVER_ERROR, + MISSING_ID, + FAILED_TO_DELETE_DATA, + FAILED_TO_UPDATE_DATA, + INVALID_DATA, + SUCCESS, + CREATED, +} = RESPONSE; const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env; async function authenticateSuperuser(): Promise { - if (!EMAIL || !PASSWORD) { - throw new Error("Environment variables EMAIL and PASSWORD must be set"); - } - if (!pb.authStore.isValid) { - await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); - } + if (!EMAIL || !PASSWORD) { + throw new Error("Environment variables EMAIL and PASSWORD must be set"); + } + if (!pb.authStore.isValid) { + await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); + } } export async function GET( - _req: Request, - context: { params: Promise<{ id: string }> } + _req: Request, + context: { params: Promise<{ id: string }> }, ): Promise { - try { - await authenticateSuperuser(); + try { + await authenticateSuperuser(); - const id = (await context.params)?.id; - if (!id) { - return ResponseHelper.error(MISSING_ID); - } + const id = (await context.params)?.id; + if (!id) { + return ResponseHelper.error(MISSING_ID); + } - const record: Budgetable = await pb.collection(COLLECTION).getOne(id); - return ResponseHelper.success(record, CREATED.STATUS); - } catch (error) { - console.error("Error fetching data:", error); - return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); - } + const record: Budgetable = await pb + .collection(COLLECTION) + .getOne(id); + return ResponseHelper.success(record, CREATED.STATUS); + } catch (error) { + console.error("Error fetching data:", error); + return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); + } } export async function DELETE( - _req: Request, - context: { params: Promise<{ id: string }> } + _req: Request, + context: { params: Promise<{ id: string }> }, ): Promise { - try { - await authenticateSuperuser(); + try { + await authenticateSuperuser(); - const id = (await context.params)?.id; - if (!id) { - return ResponseHelper.error(MISSING_ID); - } + const id = (await context.params)?.id; + if (!id) { + return ResponseHelper.error(MISSING_ID); + } - await pb.collection(COLLECTION).delete(id); - return ResponseHelper.success(SUCCESS.MESSAGE); - } catch (error) { - console.error("Error deleting data:", error); - return ResponseHelper.error(FAILED_TO_DELETE_DATA, error); - } + await pb.collection(COLLECTION).delete(id); + return ResponseHelper.success(SUCCESS.MESSAGE); + } catch (error) { + console.error("Error deleting data:", error); + return ResponseHelper.error(FAILED_TO_DELETE_DATA, error); + } } export async function PUT( - req: Request, - context: { params: Promise<{ id: string }> } + req: Request, + context: { params: Promise<{ id: string }> }, ): Promise { - try { - await authenticateSuperuser(); + try { + await authenticateSuperuser(); - const id = (await context.params)?.id; - if (!id) { - return ResponseHelper.error(MISSING_ID); - } + const id = (await context.params)?.id; + if (!id) { + return ResponseHelper.error(MISSING_ID); + } - const body: Partial = await req.json(); - if (!body.title || typeof body.price !== "number") { - return ResponseHelper.error(INVALID_DATA); - } + const body: Partial = await req.json(); + if (!body.title || typeof body.price !== "number") { + return ResponseHelper.error(INVALID_DATA); + } - const updatedRecord: Budgetable = await pb - .collection(COLLECTION) - .update(id, body); + const updatedRecord: Budgetable = await pb + .collection(COLLECTION) + .update(id, body); - return ResponseHelper.success(updatedRecord); - } catch (error) { - console.error("Error updating data:", error); - return ResponseHelper.error(FAILED_TO_UPDATE_DATA, error); - } + return ResponseHelper.success(updatedRecord); + } catch (error) { + console.error("Error updating data:", error); + return ResponseHelper.error(FAILED_TO_UPDATE_DATA, error); + } } diff --git a/src/app/pocketbase/route.ts b/src/app/pocketbase/route.ts index 60e604d..da776bb 100644 --- a/src/app/pocketbase/route.ts +++ b/src/app/pocketbase/route.ts @@ -1,48 +1,48 @@ import pb from "@/lib/pocketbase"; import { ResponseHelper } from "@/lib/helper"; import { RESPONSE } from "@/lib/const"; -import { Budgetable } from "@/lib/utils"; +import type { Budgetable } from "@/lib/utils"; const { INTERNAL_SERVER_ERROR } = RESPONSE; const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env; async function authenticateSuperuser(): Promise { - if (!EMAIL || !PASSWORD) { - throw new Error("Environment variables EMAIL and PASSWORD must be set"); - } - if (!pb.authStore.isValid) { - await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); - } + if (!EMAIL || !PASSWORD) { + throw new Error("Environment variables EMAIL and PASSWORD must be set"); + } + if (!pb.authStore.isValid) { + await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); + } } export async function GET(): Promise { - try { - await authenticateSuperuser(); + try { + await authenticateSuperuser(); - const records: Budgetable[] = await pb - .collection(COLLECTION) - .getFullList(); + const records: Budgetable[] = await pb + .collection(COLLECTION) + .getFullList(); - return ResponseHelper.success(records); - } catch (error) { - console.error("Error fetching data:", error); - return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); - } + return ResponseHelper.success(records); + } catch (error) { + console.error("Error fetching data:", error); + return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); + } } export async function POST(req: Request): Promise { - try { - await authenticateSuperuser(); + try { + await authenticateSuperuser(); - const data: Omit = await req.json(); + const data: Omit = await req.json(); - const record: Budgetable = await pb - .collection(COLLECTION) - .create(data); + const record: Budgetable = await pb + .collection(COLLECTION) + .create(data); - return ResponseHelper.success(record); - } catch (error) { - console.error("Error adding data:", error); - return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); - } + return ResponseHelper.success(record); + } catch (error) { + console.error("Error adding data:", error); + return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); + } } diff --git a/src/components/header.tsx b/src/components/header.tsx index 1b6aa39..0fe1115 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -18,7 +18,7 @@ const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => ( ); diff --git a/src/lib/const.ts b/src/lib/const.ts index f98faee..0f11352 100644 --- a/src/lib/const.ts +++ b/src/lib/const.ts @@ -1,29 +1,41 @@ type ResponseConstant = { STATUS: number; MESSAGE: string; - }; +}; - type ResponseMap = { +type ResponseMap = { [key: string]: ResponseConstant; - }; +}; - export enum HttpStatus { +export enum HttpStatus { OK = 200, CREATED = 201, BAD_REQUEST = 400, INTERNAL_SERVER_ERROR = 500, - } +} - function createResponse(status: number, message: string): ResponseConstant { +function createResponse(status: number, message: string): ResponseConstant { return { STATUS: status, MESSAGE: message }; - } +} - export const RESPONSE: ResponseMap = { - INTERNAL_SERVER_ERROR: createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error."), +export const RESPONSE: ResponseMap = { + INTERNAL_SERVER_ERROR: createResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "Internal server error.", + ), MISSING_ID: createResponse(HttpStatus.BAD_REQUEST, "Missing ID in request."), - FAILED_TO_DELETE_DATA: createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete data."), - FAILED_TO_UPDATE_DATA: createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update data."), - INVALID_DATA: createResponse(HttpStatus.BAD_REQUEST, "Invalid data provided."), + FAILED_TO_DELETE_DATA: createResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to delete data.", + ), + FAILED_TO_UPDATE_DATA: createResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to update data.", + ), + INVALID_DATA: createResponse( + HttpStatus.BAD_REQUEST, + "Invalid data provided.", + ), SUCCESS: createResponse(HttpStatus.OK, "Operation completed successfully."), CREATED: createResponse(HttpStatus.CREATED, "Resource created successfully."), - }; +}; diff --git a/src/lib/helper.ts b/src/lib/helper.ts index c13a1d0..94dcd27 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -1,10 +1,10 @@ import { RESPONSE } from "@/lib/const"; interface ErrorResponse { - error: { - message: string; - details?: unknown; - }; + error: { + message: string; + details?: unknown; + }; } type SuccessResponse = T; @@ -12,35 +12,35 @@ type SuccessResponse = T; type ResponseData = SuccessResponse | ErrorResponse; interface ResponseOptions { - status?: number; + status?: number; } export class ResponseHelper { - private data: ResponseData; - private status: number; + private data: ResponseData; + private status: number; - constructor(data: ResponseData, options: ResponseOptions = {}) { - this.data = data; - this.status = options.status || 200; - } + constructor(data: ResponseData, options: ResponseOptions = {}) { + this.data = data; + this.status = options.status || 200; + } - static success(data: T, status = RESPONSE.SUCCESS.STATUS): Response { - return new ResponseHelper(data, { status }).toResponse(); - } + static success(data: T, status = RESPONSE.SUCCESS.STATUS): Response { + return new ResponseHelper(data, { status }).toResponse(); + } - static error( - constant: (typeof RESPONSE)[keyof typeof RESPONSE], - details?: unknown - ): Response { - return new ResponseHelper( - { error: { message: constant.MESSAGE, details } }, - { status: constant.STATUS } - ).toResponse(); - } + static error( + constant: (typeof RESPONSE)[keyof typeof RESPONSE], + details?: unknown, + ): Response { + return new ResponseHelper( + { error: { message: constant.MESSAGE, details } }, + { status: constant.STATUS }, + ).toResponse(); + } - toResponse(): Response { - return Response.json(this.data, { - status: this.status, - }); - } + toResponse(): Response { + return Response.json(this.data, { + status: this.status, + }); + } }