mirror of
https://github.com/skidoodle/budgetable.git
synced 2025-02-15 03:39:14 +01:00
feat: enhance ui components and response handling
This commit is contained in:
parent
78fbcebe91
commit
83b3a747a1
11 changed files with 296 additions and 328 deletions
|
@ -48,6 +48,7 @@ const nextConfig: NextConfig = {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
poweredByHeader: false,
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
240
src/app/page.tsx
240
src/app/page.tsx
|
@ -1,150 +1,132 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import TableWrapper from "@/components/table-wrapper";
|
import TableWrapper from "@/components/table-wrapper";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import TotalDisplay from "@/components/total-display";
|
import TotalDisplay from "@/components/total-display";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type Budgetable, areRowsEqual } from "@/lib/utils";
|
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() {
|
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 [loading, setLoading] = useState(false);
|
const [newRow, setNewRow] = useState<Budgetable>(DEFAULT_NEW_ROW);
|
||||||
const [newRow, setNewRow] = useState<Budgetable>({
|
const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<string | null>(null);
|
||||||
id: "",
|
|
||||||
title: "",
|
|
||||||
price: 0,
|
|
||||||
link: "",
|
|
||||||
note: "",
|
|
||||||
status: "Unpaid",
|
|
||||||
});
|
|
||||||
const [recentlyUpdatedRowId, setRecentlyUpdatedRowId] = useState<
|
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchData = useCallback(async () => {
|
||||||
async function fetchData() {
|
try {
|
||||||
setLoading(true);
|
const res = await fetch(ENDPOINT);
|
||||||
try {
|
if (!res.ok) throw new Error("Failed to fetch data");
|
||||||
const res = await fetch("/pocketbase");
|
const records: Budgetable[] = await res.json();
|
||||||
if (!res.ok) throw new Error("Failed to fetch data");
|
setData(records);
|
||||||
const records: Budgetable[] = await res.json();
|
} catch (err) {
|
||||||
setData(records);
|
toast.error("Error fetching data. Please try again later.");
|
||||||
} catch (err) {
|
console.error(err);
|
||||||
toast.error("Error fetching data. Please try again later.");
|
}
|
||||||
console.error("Error fetching data:", err);
|
}, []);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData();
|
useEffect(() => {
|
||||||
}, []);
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
const handleSave = async (
|
const handleSave = useCallback(async (updatedRow: Budgetable, originalRow: Budgetable) => {
|
||||||
updatedRow: Budgetable,
|
if (areRowsEqual(updatedRow, originalRow)) return;
|
||||||
originalRow: Budgetable,
|
|
||||||
) => {
|
|
||||||
if (areRowsEqual(updatedRow, originalRow)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/pocketbase/${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();
|
|
||||||
setData((prev) =>
|
|
||||||
prev.map((row) => (row.id === updatedRow.id ? updatedData : row)),
|
|
||||||
);
|
|
||||||
|
|
||||||
setRecentlyUpdatedRowId(updatedRow.id);
|
const updatedData = await res.json();
|
||||||
setTimeout(() => setRecentlyUpdatedRowId(null), 500);
|
setData((prev) => prev.map((row) => (row.id === updatedRow.id ? updatedData : row)));
|
||||||
toast.success("Row updated successfully!");
|
|
||||||
} catch (err) {
|
|
||||||
toast.error("Error updating row. Please try again.");
|
|
||||||
console.error("Error updating row:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddRow = async () => {
|
setRecentlyUpdatedRowId(updatedRow.id);
|
||||||
if (!newRow.title || newRow.price <= 0) {
|
setTimeout(() => setRecentlyUpdatedRowId(null), 500);
|
||||||
toast("Title and price are required.");
|
toast.success("Row updated successfully!");
|
||||||
return;
|
} catch (err) {
|
||||||
}
|
toast.error("Error updating row. Please try again.");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
try {
|
const handleAddRow = useCallback(async () => {
|
||||||
const res = await fetch("/pocketbase", {
|
if (!newRow.title || newRow.price <= 0) {
|
||||||
method: "POST",
|
toast.error("Title and price are required.");
|
||||||
headers: { "Content-Type": "application/json" },
|
return;
|
||||||
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 handleDeleteRow = async (id: string) => {
|
try {
|
||||||
try {
|
const res = await fetch(ENDPOINT, {
|
||||||
const res = await fetch(`/pocketbase/${id}`, { method: "DELETE" });
|
method: "POST",
|
||||||
if (!res.ok) throw new Error("Failed to delete row");
|
headers: { "Content-Type": "application/json" },
|
||||||
setData((prev) => prev.filter((row) => row.id !== id));
|
body: JSON.stringify(newRow),
|
||||||
toast.success("Row deleted successfully!");
|
});
|
||||||
} catch (err) {
|
if (!res.ok) throw new Error("Failed to add row");
|
||||||
toast.error("Error deleting row. Please try again.");
|
|
||||||
console.error("Error deleting row:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleStatus = async (row: Budgetable) => {
|
const record: Budgetable = await res.json();
|
||||||
const updatedStatus: "Paid" | "Unpaid" =
|
setData((prev) => [...prev, record]);
|
||||||
row.status === "Paid" ? "Unpaid" : "Paid";
|
setNewRow(DEFAULT_NEW_ROW);
|
||||||
const updatedRow: Budgetable = { ...row, status: updatedStatus };
|
toast.success("Row added successfully!");
|
||||||
await handleSave(updatedRow, row);
|
} catch (err) {
|
||||||
setData((prev) =>
|
toast.error("Error adding row. Please try again.");
|
||||||
prev.map((item) => (item.id === row.id ? updatedRow : item)),
|
console.error(err);
|
||||||
);
|
}
|
||||||
};
|
}, [newRow]);
|
||||||
|
|
||||||
const total = data.reduce(
|
const handleDeleteRow = useCallback(async (id: string) => {
|
||||||
(sum, item) => sum + (item.status === "Unpaid" ? item.price : 0),
|
try {
|
||||||
0,
|
const res = await fetch(`${ENDPOINT}/${id}`, { method: "DELETE" });
|
||||||
);
|
if (!res.ok) throw new Error("Failed to delete row");
|
||||||
|
|
||||||
return (
|
setData((prev) => prev.filter((row) => row.id !== id));
|
||||||
<main className="container mx-auto p-4 max-w-5xl">
|
toast.success("Row deleted successfully!");
|
||||||
{loading}
|
} catch (err) {
|
||||||
<Header isEditing={isEditing} setIsEditing={setIsEditing} />
|
toast.error("Error deleting row. Please try again.");
|
||||||
<TotalDisplay total={total} />
|
console.error(err);
|
||||||
<TableWrapper
|
}
|
||||||
data={data}
|
}, []);
|
||||||
isEditing={isEditing}
|
|
||||||
setData={setData}
|
const toggleStatus = useCallback(async (row: Budgetable) => {
|
||||||
newRow={newRow}
|
const updatedStatus = row.status === "Paid" ? "Unpaid" : "Paid";
|
||||||
setNewRow={setNewRow}
|
const updatedRow: Budgetable = { ...row, status: updatedStatus as "Paid" | "Unpaid" };
|
||||||
recentlyUpdatedRowId={recentlyUpdatedRowId}
|
await handleSave(updatedRow, row);
|
||||||
handleSave={handleSave}
|
}, [handleSave]);
|
||||||
handleAddRow={handleAddRow}
|
|
||||||
handleDeleteRow={handleDeleteRow}
|
const total = data.reduce(
|
||||||
toggleStatus={toggleStatus}
|
(sum, item) => sum + (item.status === "Unpaid" ? item.price : 0),
|
||||||
/>
|
0,
|
||||||
</main>
|
);
|
||||||
);
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,161 +1,84 @@
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase";
|
||||||
|
import { ResponseHelper } from "@/lib/helper";
|
||||||
|
import { RESPONSE } from "@/lib/const";
|
||||||
|
import { Budgetable } from "@/lib/utils";
|
||||||
|
|
||||||
|
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() {
|
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> {
|
||||||
try {
|
try {
|
||||||
await authenticateSuperuser();
|
await authenticateSuperuser();
|
||||||
|
|
||||||
const id = (await context.params)?.id;
|
const id = (await context.params)?.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return Response.json(
|
return ResponseHelper.error(MISSING_ID);
|
||||||
{
|
}
|
||||||
error: {
|
|
||||||
message: "Missing ID in request",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const record = await pb.collection(COLLECTION).getOne(id);
|
const record: Budgetable = await pb.collection<Budgetable>(COLLECTION).getOne(id);
|
||||||
return Response.json(record, {
|
return ResponseHelper.success<Budgetable>(record, CREATED.STATUS);
|
||||||
status: 200,
|
} catch (error) {
|
||||||
headers: { "Content-Type": "application/json" },
|
console.error("Error fetching data:", error);
|
||||||
});
|
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
|
||||||
} catch (error) {
|
}
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "Failed to fetch data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
_req: Request,
|
_req: Request,
|
||||||
context: { params: Promise<{ id: string }> },
|
context: { params: Promise<{ id: string }> }
|
||||||
) {
|
): 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 Response.json(
|
return ResponseHelper.error(MISSING_ID);
|
||||||
{
|
}
|
||||||
error: {
|
|
||||||
message: "Missing ID in request",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await pb.collection(COLLECTION).delete(id);
|
await pb.collection(COLLECTION).delete(id);
|
||||||
return Response.json(
|
return ResponseHelper.success(SUCCESS.MESSAGE);
|
||||||
{
|
} catch (error) {
|
||||||
success: true,
|
console.error("Error deleting data:", error);
|
||||||
},
|
return ResponseHelper.error(FAILED_TO_DELETE_DATA, error);
|
||||||
{
|
}
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting data:", error);
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "Failed to delete data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
req: Request,
|
req: Request,
|
||||||
context: { params: Promise<{ id: string }> },
|
context: { params: Promise<{ id: string }> }
|
||||||
) {
|
): 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 Response.json(
|
return ResponseHelper.error(MISSING_ID);
|
||||||
{
|
}
|
||||||
error: {
|
|
||||||
message: "Missing ID in request",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = 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 Response.json(
|
return ResponseHelper.error(INVALID_DATA);
|
||||||
{
|
}
|
||||||
error: {
|
|
||||||
message: "Invalid data provided",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedRecord = await pb.collection(COLLECTION).update(id, body);
|
const updatedRecord: Budgetable = await pb
|
||||||
return Response.json(updatedRecord, {
|
.collection<Budgetable>(COLLECTION)
|
||||||
status: 200,
|
.update(id, body);
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
return ResponseHelper.success<Budgetable>(updatedRecord);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating data:", error);
|
console.error("Error updating data:", error);
|
||||||
return Response.json(
|
return ResponseHelper.error(FAILED_TO_UPDATE_DATA, error);
|
||||||
{
|
}
|
||||||
error: {
|
|
||||||
message: "Failed to update data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,48 @@
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase";
|
||||||
|
import { ResponseHelper } from "@/lib/helper";
|
||||||
|
import { RESPONSE } from "@/lib/const";
|
||||||
|
import { Budgetable } from "@/lib/utils";
|
||||||
|
|
||||||
|
const { INTERNAL_SERVER_ERROR } = RESPONSE;
|
||||||
const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env;
|
const { EMAIL, PASSWORD, COLLECTION = "budgetable" } = process.env;
|
||||||
|
|
||||||
async function authenticateSuperuser() {
|
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(): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
await authenticateSuperuser();
|
await authenticateSuperuser();
|
||||||
const records = await pb.collection(COLLECTION).getFullList();
|
|
||||||
return Response.json(records, {
|
const records: Budgetable[] = await pb
|
||||||
status: 200,
|
.collection<Budgetable>(COLLECTION)
|
||||||
headers: { "Content-Type": "application/json" },
|
.getFullList();
|
||||||
});
|
|
||||||
} catch (error) {
|
return ResponseHelper.success<Budgetable[]>(records);
|
||||||
console.error("Error fetching data:", error);
|
} catch (error) {
|
||||||
return Response.json(
|
console.error("Error fetching data:", error);
|
||||||
{
|
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
|
||||||
error: {
|
}
|
||||||
message: "Failed to fetch data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
await authenticateSuperuser();
|
await authenticateSuperuser();
|
||||||
const data = await req.json();
|
|
||||||
const record = await pb.collection(COLLECTION).create(data);
|
const data: Omit<Budgetable, "id"> = await req.json();
|
||||||
return Response.json(record, {
|
|
||||||
status: 201,
|
const record: Budgetable = await pb
|
||||||
headers: { "Content-Type": "application/json" },
|
.collection<Budgetable>(COLLECTION)
|
||||||
});
|
.create(data);
|
||||||
} catch (error) {
|
|
||||||
console.error("Error adding data:", error);
|
return ResponseHelper.success<Budgetable>(record);
|
||||||
return Response.json(
|
} catch (error) {
|
||||||
{
|
console.error("Error adding data:", error);
|
||||||
error: {
|
return ResponseHelper.error(INTERNAL_SERVER_ERROR, error);
|
||||||
message: "Failed to add data",
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface AppHeaderProps {
|
||||||
|
|
||||||
const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
|
const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h1 className="text-3xl font-bold">
|
<h1 className="text-4xl font-bold">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="text-blue-500 hover:text-blue-600 transition-colors"
|
className="text-blue-500 hover:text-blue-600 transition-colors"
|
||||||
|
@ -17,8 +17,8 @@ const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
|
||||||
Budgetable
|
Budgetable
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<Button variant="ghost" onClick={() => setIsEditing(!isEditing)}>
|
<Button variant="ghost" onClick={() => setIsEditing(!isEditing)} size="lg">
|
||||||
{isEditing ? "Lock" : "Unlock"} Editing
|
<span className='text-lg'>{isEditing ? "Lock" : "Unlock"} Editing</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface StatusBadgeProps {
|
||||||
const StatusBadge = ({ status, toggleStatus }: StatusBadgeProps) => (
|
const StatusBadge = ({ status, toggleStatus }: StatusBadgeProps) => (
|
||||||
<Badge
|
<Badge
|
||||||
variant={status === "Paid" ? "paid" : "unpaid"}
|
variant={status === "Paid" ? "paid" : "unpaid"}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer text-[16px]"
|
||||||
onClick={toggleStatus}
|
onClick={toggleStatus}
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
|
|
|
@ -84,7 +84,7 @@ const TRow = ({
|
||||||
onBlur={() => handleSave(row, originalRow)}
|
onBlur={() => handleSave(row, originalRow)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>{row.price.toLocaleString()} HUF</span>
|
<span>{row.price.toLocaleString()}</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
|
@ -37,7 +37,7 @@ const TWrapper = ({
|
||||||
handleDeleteRow,
|
handleDeleteRow,
|
||||||
toggleStatus,
|
toggleStatus,
|
||||||
}: TWrapperProps) => (
|
}: TWrapperProps) => (
|
||||||
<Table>
|
<Table className="text-lg">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Title</TableHead>
|
<TableHead>Title</TableHead>
|
||||||
|
|
|
@ -5,7 +5,7 @@ interface TotalDisplayProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TotalDisplay = ({ total }: TotalDisplayProps) => (
|
const TotalDisplay = ({ total }: TotalDisplayProps) => (
|
||||||
<div className="mx-2 text-left font-bold text-lg">
|
<div className="mx-2 text-left font-bold text-2xl">
|
||||||
Total: <NumberFlow value={total} />
|
Total: <NumberFlow value={total} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
29
src/lib/const.ts
Normal file
29
src/lib/const.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
type ResponseConstant = {
|
||||||
|
STATUS: number;
|
||||||
|
MESSAGE: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseMap = {
|
||||||
|
[key: string]: ResponseConstant;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum HttpStatus {
|
||||||
|
OK = 200,
|
||||||
|
CREATED = 201,
|
||||||
|
BAD_REQUEST = 400,
|
||||||
|
INTERNAL_SERVER_ERROR = 500,
|
||||||
|
}
|
||||||
|
|
||||||
|
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."),
|
||||||
|
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."),
|
||||||
|
SUCCESS: createResponse(HttpStatus.OK, "Operation completed successfully."),
|
||||||
|
CREATED: createResponse(HttpStatus.CREATED, "Resource created successfully."),
|
||||||
|
};
|
46
src/lib/helper.ts
Normal file
46
src/lib/helper.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { RESPONSE } from "@/lib/const";
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
error: {
|
||||||
|
message: string;
|
||||||
|
details?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuccessResponse<T> = T;
|
||||||
|
|
||||||
|
type ResponseData<T> = SuccessResponse<T> | ErrorResponse;
|
||||||
|
|
||||||
|
interface ResponseOptions {
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResponseHelper<T = unknown> {
|
||||||
|
private data: ResponseData<T>;
|
||||||
|
private status: number;
|
||||||
|
|
||||||
|
constructor(data: ResponseData<T>, options: ResponseOptions = {}) {
|
||||||
|
this.data = data;
|
||||||
|
this.status = options.status || 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
static success<T>(data: T, status = RESPONSE.SUCCESS.STATUS): Response {
|
||||||
|
return new ResponseHelper<T>(data, { status }).toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(
|
||||||
|
constant: (typeof RESPONSE)[keyof typeof RESPONSE],
|
||||||
|
details?: unknown
|
||||||
|
): Response {
|
||||||
|
return new ResponseHelper<ErrorResponse>(
|
||||||
|
{ error: { message: constant.MESSAGE, details } },
|
||||||
|
{ status: constant.STATUS }
|
||||||
|
).toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
toResponse(): Response {
|
||||||
|
return Response.json(this.data, {
|
||||||
|
status: this.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue