formatted all

This commit is contained in:
skidoodle 2025-01-03 18:54:12 +01:00
parent 83b3a747a1
commit c9561cb795
Signed by: albert
GPG key ID: A06E3070D7D55BF2
6 changed files with 263 additions and 228 deletions

View file

@ -8,125 +8,138 @@ import { toast } from "sonner";
import { type Budgetable, areRowsEqual } from "@/lib/utils"; import { type Budgetable, areRowsEqual } from "@/lib/utils";
const DEFAULT_NEW_ROW: Budgetable = { const DEFAULT_NEW_ROW: Budgetable = {
id: "", id: "",
title: "", title: "",
price: 0, price: 0,
link: "", link: "",
note: "", note: "",
status: "Unpaid", status: "Unpaid",
}; };
const ENDPOINT = "/pocketbase"; const ENDPOINT = "/pocketbase";
export default function App() { export default function App() {
const [data, setData] = useState<Budgetable[]>([]); const [data, setData] = useState<Budgetable[]>([]);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [newRow, setNewRow] = useState<Budgetable>(DEFAULT_NEW_ROW); const [newRow, setNewRow] = useState<Budgetable>(DEFAULT_NEW_ROW);
const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<string | null>(null); const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<
string | null
>(null);
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
const res = await fetch(ENDPOINT); const res = await fetch(ENDPOINT);
if (!res.ok) throw new Error("Failed to fetch data"); if (!res.ok) throw new Error("Failed to fetch data");
const records: Budgetable[] = await res.json(); const records: Budgetable[] = await res.json();
setData(records); setData(records);
} catch (err) { } catch (err) {
toast.error("Error fetching data. Please try again later."); toast.error("Error fetching data. Please try again later.");
console.error(err); console.error(err);
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [fetchData]); }, [fetchData]);
const handleSave = useCallback(async (updatedRow: Budgetable, originalRow: Budgetable) => { const handleSave = useCallback(
if (areRowsEqual(updatedRow, originalRow)) return; async (updatedRow: Budgetable, originalRow: Budgetable) => {
if (areRowsEqual(updatedRow, originalRow)) return;
try { try {
const res = await fetch(`${ENDPOINT}/${updatedRow.id}`, { const res = await fetch(`${ENDPOINT}/${updatedRow.id}`, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(updatedRow), body: JSON.stringify(updatedRow),
}); });
if (!res.ok) throw new Error("Failed to update row"); if (!res.ok) throw new Error("Failed to update row");
const updatedData = await res.json(); const updatedData = await res.json();
setData((prev) => prev.map((row) => (row.id === updatedRow.id ? updatedData : row))); setData((prev) =>
prev.map((row) => (row.id === updatedRow.id ? updatedData : row)),
);
setRecentlyUpdatedRowId(updatedRow.id); setRecentlyUpdatedRowId(updatedRow.id);
setTimeout(() => setRecentlyUpdatedRowId(null), 500); setTimeout(() => setRecentlyUpdatedRowId(null), 500);
toast.success("Row updated successfully!"); toast.success("Row updated successfully!");
} catch (err) { } catch (err) {
toast.error("Error updating row. Please try again."); toast.error("Error updating row. Please try again.");
console.error(err); console.error(err);
} }
}, []); },
[],
);
const handleAddRow = useCallback(async () => { const handleAddRow = useCallback(async () => {
if (!newRow.title || newRow.price <= 0) { if (!newRow.title || newRow.price <= 0) {
toast.error("Title and price are required."); toast.error("Title and price are required.");
return; return;
} }
try { try {
const res = await fetch(ENDPOINT, { const res = await fetch(ENDPOINT, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(newRow), body: JSON.stringify(newRow),
}); });
if (!res.ok) throw new Error("Failed to add row"); if (!res.ok) throw new Error("Failed to add row");
const record: Budgetable = await res.json(); const record: Budgetable = await res.json();
setData((prev) => [...prev, record]); setData((prev) => [...prev, record]);
setNewRow(DEFAULT_NEW_ROW); setNewRow(DEFAULT_NEW_ROW);
toast.success("Row added successfully!"); toast.success("Row added successfully!");
} catch (err) { } catch (err) {
toast.error("Error adding row. Please try again."); toast.error("Error adding row. Please try again.");
console.error(err); console.error(err);
} }
}, [newRow]); }, [newRow]);
const handleDeleteRow = useCallback(async (id: string) => { const handleDeleteRow = useCallback(async (id: string) => {
try { try {
const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" }); const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Failed to delete row"); if (!res.ok) throw new Error("Failed to delete row");
setData((prev) => prev.filter((row) => row.id !== id)); setData((prev) => prev.filter((row) => row.id !== id));
toast.success("Row deleted successfully!"); toast.success("Row deleted successfully!");
} catch (err) { } catch (err) {
toast.error("Error deleting row. Please try again."); toast.error("Error deleting row. Please try again.");
console.error(err); console.error(err);
} }
}, []); }, []);
const toggleStatus = useCallback(async (row: Budgetable) => { const toggleStatus = useCallback(
const updatedStatus = row.status === "Paid" ? "Unpaid" : "Paid"; async (row: Budgetable) => {
const updatedRow: Budgetable = { ...row, status: updatedStatus as "Paid" | "Unpaid" }; const updatedStatus = row.status === "Paid" ? "Unpaid" : "Paid";
await handleSave(updatedRow, row); const updatedRow: Budgetable = {
}, [handleSave]); ...row,
status: updatedStatus as "Paid" | "Unpaid",
};
await handleSave(updatedRow, row);
},
[handleSave],
);
const total = data.reduce( const total = data.reduce(
(sum, item) => sum + (item.status === "Unpaid" ? item.price : 0), (sum, item) => sum + (item.status === "Unpaid" ? item.price : 0),
0, 0,
); );
return ( return (
<main className="container mx-auto p-4 max-w-7xl"> <main className="container mx-auto p-4 max-w-7xl">
<Header isEditing={isEditing} setIsEditing={setIsEditing} /> <Header isEditing={isEditing} setIsEditing={setIsEditing} />
<TotalDisplay total={total} /> <TotalDisplay total={total} />
<TableWrapper <TableWrapper
data={data} data={data}
isEditing={isEditing} isEditing={isEditing}
setData={setData} setData={setData}
newRow={newRow} newRow={newRow}
setNewRow={setNewRow} setNewRow={setNewRow}
recentlyUpdatedRowId={recentlyUpdatedRowId} recentlyUpdatedRowId={recentlyUpdatedRowId}
handleSave={handleSave} handleSave={handleSave}
handleAddRow={handleAddRow} handleAddRow={handleAddRow}
handleDeleteRow={handleDeleteRow} handleDeleteRow={handleDeleteRow}
toggleStatus={toggleStatus} toggleStatus={toggleStatus}
/> />
</main> </main>
); );
} }

View file

@ -1,84 +1,94 @@
import pb from "@/lib/pocketbase"; import pb from "@/lib/pocketbase";
import { ResponseHelper } from "@/lib/helper"; import { ResponseHelper } from "@/lib/helper";
import { RESPONSE } from "@/lib/const"; 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; const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env;
async function authenticateSuperuser(): Promise<void> { async function authenticateSuperuser(): Promise<void> {
if (!EMAIL || !PASSWORD) { if (!EMAIL || !PASSWORD) {
throw new Error("Environment variables EMAIL and PASSWORD must be set"); throw new Error("Environment variables EMAIL and PASSWORD must be set");
} }
if (!pb.authStore.isValid) { if (!pb.authStore.isValid) {
await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD);
} }
} }
export async function GET( export async function GET(
_req: Request, _req: Request,
context: { params: Promise<{ id: string }> } context: { params: Promise<{ id: string }> },
): Promise<Response> { ): Promise<Response> {
try { try {
await authenticateSuperuser(); await authenticateSuperuser();
const id = (await context.params)?.id; const id = (await context.params)?.id;
if (!id) { if (!id) {
return ResponseHelper.error(MISSING_ID); return ResponseHelper.error(MISSING_ID);
} }
const record: Budgetable = await pb.collection<Budgetable>(COLLECTION).getOne(id); const record: Budgetable = await pb
return ResponseHelper.success<Budgetable>(record, CREATED.STATUS); .collection<Budgetable>(COLLECTION)
} catch (error) { .getOne(id);
console.error("Error fetching data:", error); return ResponseHelper.success<Budgetable>(record, CREATED.STATUS);
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); } catch (error) {
} console.error("Error fetching data:", error);
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
}
} }
export async function DELETE( export async function DELETE(
_req: Request, _req: Request,
context: { params: Promise<{ id: string }> } context: { params: Promise<{ id: string }> },
): Promise<Response> { ): Promise<Response> {
try { try {
await authenticateSuperuser(); await authenticateSuperuser();
const id = (await context.params)?.id; const id = (await context.params)?.id;
if (!id) { if (!id) {
return ResponseHelper.error(MISSING_ID); return ResponseHelper.error(MISSING_ID);
} }
await pb.collection(COLLECTION).delete(id); await pb.collection(COLLECTION).delete(id);
return ResponseHelper.success(SUCCESS.MESSAGE); return ResponseHelper.success(SUCCESS.MESSAGE);
} catch (error) { } catch (error) {
console.error("Error deleting data:", error); console.error("Error deleting data:", error);
return ResponseHelper.error(FAILED_TO_DELETE_DATA, error); return ResponseHelper.error(FAILED_TO_DELETE_DATA, error);
} }
} }
export async function PUT( export async function PUT(
req: Request, req: Request,
context: { params: Promise<{ id: string }> } context: { params: Promise<{ id: string }> },
): Promise<Response> { ): Promise<Response> {
try { try {
await authenticateSuperuser(); await authenticateSuperuser();
const id = (await context.params)?.id; const id = (await context.params)?.id;
if (!id) { if (!id) {
return ResponseHelper.error(MISSING_ID); return ResponseHelper.error(MISSING_ID);
} }
const body: Partial<Budgetable> = await req.json(); const body: Partial<Budgetable> = await req.json();
if (!body.title || typeof body.price !== "number") { if (!body.title || typeof body.price !== "number") {
return ResponseHelper.error(INVALID_DATA); return ResponseHelper.error(INVALID_DATA);
} }
const updatedRecord: Budgetable = await pb const updatedRecord: Budgetable = await pb
.collection<Budgetable>(COLLECTION) .collection<Budgetable>(COLLECTION)
.update(id, body); .update(id, body);
return ResponseHelper.success<Budgetable>(updatedRecord); return ResponseHelper.success<Budgetable>(updatedRecord);
} catch (error) { } catch (error) {
console.error("Error updating data:", error); console.error("Error updating data:", error);
return ResponseHelper.error(FAILED_TO_UPDATE_DATA, error); return ResponseHelper.error(FAILED_TO_UPDATE_DATA, error);
} }
} }

View file

@ -1,48 +1,48 @@
import pb from "@/lib/pocketbase"; import pb from "@/lib/pocketbase";
import { ResponseHelper } from "@/lib/helper"; import { ResponseHelper } from "@/lib/helper";
import { RESPONSE } from "@/lib/const"; import { RESPONSE } from "@/lib/const";
import { Budgetable } from "@/lib/utils"; import type { Budgetable } from "@/lib/utils";
const { INTERNAL_SERVER_ERROR } = RESPONSE; const { INTERNAL_SERVER_ERROR } = RESPONSE;
const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env; const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env;
async function authenticateSuperuser(): Promise<void> { async function authenticateSuperuser(): Promise<void> {
if (!EMAIL || !PASSWORD) { if (!EMAIL || !PASSWORD) {
throw new Error("Environment variables EMAIL and PASSWORD must be set"); throw new Error("Environment variables EMAIL and PASSWORD must be set");
} }
if (!pb.authStore.isValid) { if (!pb.authStore.isValid) {
await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD); await pb.collection("_superusers").authWithPassword(EMAIL, PASSWORD);
} }
} }
export async function GET(): Promise<Response> { export async function GET(): Promise<Response> {
try { try {
await authenticateSuperuser(); await authenticateSuperuser();
const records: Budgetable[] = await pb const records: Budgetable[] = await pb
.collection<Budgetable>(COLLECTION) .collection<Budgetable>(COLLECTION)
.getFullList(); .getFullList();
return ResponseHelper.success<Budgetable[]>(records); return ResponseHelper.success<Budgetable[]>(records);
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
} }
} }
export async function POST(req: Request): Promise<Response> { export async function POST(req: Request): Promise<Response> {
try { try {
await authenticateSuperuser(); await authenticateSuperuser();
const data: Omit<Budgetable, "id"> = await req.json(); const data: Omit<Budgetable, "id"> = await req.json();
const record: Budgetable = await pb const record: Budgetable = await pb
.collection<Budgetable>(COLLECTION) .collection<Budgetable>(COLLECTION)
.create(data); .create(data);
return ResponseHelper.success<Budgetable>(record); return ResponseHelper.success<Budgetable>(record);
} catch (error) { } catch (error) {
console.error("Error adding data:", error); console.error("Error adding data:", error);
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error); return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
} }
} }

View file

@ -18,7 +18,7 @@ const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
</Link> </Link>
</h1> </h1>
<Button variant="ghost" onClick={() => setIsEditing(!isEditing)} size="lg"> <Button variant="ghost" onClick={() => setIsEditing(!isEditing)} size="lg">
<span className='text-lg'>{isEditing ? "Lock" : "Unlock"} Editing</span> <span className="text-lg">{isEditing ? "Lock" : "Unlock"} Editing</span>
</Button> </Button>
</div> </div>
); );

View file

@ -1,29 +1,41 @@
type ResponseConstant = { type ResponseConstant = {
STATUS: number; STATUS: number;
MESSAGE: string; MESSAGE: string;
}; };
type ResponseMap = { type ResponseMap = {
[key: string]: ResponseConstant; [key: string]: ResponseConstant;
}; };
export enum HttpStatus { export enum HttpStatus {
OK = 200, OK = 200,
CREATED = 201, CREATED = 201,
BAD_REQUEST = 400, BAD_REQUEST = 400,
INTERNAL_SERVER_ERROR = 500, INTERNAL_SERVER_ERROR = 500,
} }
function createResponse(status: number, message: string): ResponseConstant { function createResponse(status: number, message: string): ResponseConstant {
return { STATUS: status, MESSAGE: message }; return { STATUS: status, MESSAGE: message };
} }
export const RESPONSE: ResponseMap = { export const RESPONSE: ResponseMap = {
INTERNAL_SERVER_ERROR: createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error."), INTERNAL_SERVER_ERROR: createResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"Internal server error.",
),
MISSING_ID: createResponse(HttpStatus.BAD_REQUEST, "Missing ID in request."), 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_DELETE_DATA: createResponse(
FAILED_TO_UPDATE_DATA: createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update data."), HttpStatus.INTERNAL_SERVER_ERROR,
INVALID_DATA: createResponse(HttpStatus.BAD_REQUEST, "Invalid data provided."), "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."), SUCCESS: createResponse(HttpStatus.OK, "Operation completed successfully."),
CREATED: createResponse(HttpStatus.CREATED, "Resource created successfully."), CREATED: createResponse(HttpStatus.CREATED, "Resource created successfully."),
}; };

View file

@ -1,10 +1,10 @@
import { RESPONSE } from "@/lib/const"; import { RESPONSE } from "@/lib/const";
interface ErrorResponse { interface ErrorResponse {
error: { error: {
message: string; message: string;
details?: unknown; details?: unknown;
}; };
} }
type SuccessResponse<T> = T; type SuccessResponse<T> = T;
@ -12,35 +12,35 @@ type SuccessResponse<T> = T;
type ResponseData<T> = SuccessResponse<T> | ErrorResponse; type ResponseData<T> = SuccessResponse<T> | ErrorResponse;
interface ResponseOptions { interface ResponseOptions {
status?: number; status?: number;
} }
export class ResponseHelper<T = unknown> { export class ResponseHelper<T = unknown> {
private data: ResponseData<T>; private data: ResponseData<T>;
private status: number; private status: number;
constructor(data: ResponseData<T>, options: ResponseOptions = {}) { constructor(data: ResponseData<T>, options: ResponseOptions = {}) {
this.data = data; this.data = data;
this.status = options.status || 200; this.status = options.status || 200;
} }
static success<T>(data: T, status = RESPONSE.SUCCESS.STATUS): Response { static success<T>(data: T, status = RESPONSE.SUCCESS.STATUS): Response {
return new ResponseHelper<T>(data, { status }).toResponse(); return new ResponseHelper<T>(data, { status }).toResponse();
} }
static error( static error(
constant: (typeof RESPONSE)[keyof typeof RESPONSE], constant: (typeof RESPONSE)[keyof typeof RESPONSE],
details?: unknown details?: unknown,
): Response { ): Response {
return new ResponseHelper<ErrorResponse>( return new ResponseHelper<ErrorResponse>(
{ error: { message: constant.MESSAGE, details } }, { error: { message: constant.MESSAGE, details } },
{ status: constant.STATUS } { status: constant.STATUS },
).toResponse(); ).toResponse();
} }
toResponse(): Response { toResponse(): Response {
return Response.json(this.data, { return Response.json(this.data, {
status: this.status, status: this.status,
}); });
} }
} }