mirror of
https://github.com/skidoodle/budgetable.git
synced 2025-02-15 03:39:14 +01:00
refactor: update imports to use 'type' for TypeScript types and enhance tooltip functionality
This commit is contained in:
parent
8e37da9463
commit
3d082d3092
14 changed files with 367 additions and 101 deletions
|
@ -5,7 +5,7 @@ import TableWrapper from "@/components/table-wrapper";
|
|||
import Header from "@/components/header";
|
||||
import TotalDisplay from "@/components/total-display";
|
||||
import { toast } from "sonner";
|
||||
import { Budgetable, areRowsEqual } from "@/lib/utils";
|
||||
import { type Budgetable, areRowsEqual } from "@/lib/utils";
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<Budgetable[]>(() => []);
|
||||
|
@ -132,6 +132,7 @@ export default function App() {
|
|||
<main className="container mx-auto p-4 max-w-5xl">
|
||||
{loading}
|
||||
<Header isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<TotalDisplay total={total} />
|
||||
<TableWrapper
|
||||
data={data}
|
||||
isEditing={isEditing}
|
||||
|
@ -144,7 +145,6 @@ export default function App() {
|
|||
handleDeleteRow={handleDeleteRow}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
<TotalDisplay total={total} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,14 +20,17 @@ export async function GET(
|
|||
|
||||
const id = (await context.params)?.id;
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const record = await pb.collection("budgetable").getOne(id);
|
||||
|
@ -37,14 +40,17 @@ export async function GET(
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Failed to fetch data",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Failed to fetch data",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,33 +63,42 @@ export async function DELETE(
|
|||
|
||||
const id = (await context.params)?.id;
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await pb.collection("budgetable").delete(id);
|
||||
return Response.json({
|
||||
success: true,
|
||||
}, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
return Response.json(
|
||||
{
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error deleting data:", error);
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Failed to delete data",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Failed to delete data",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,26 +111,32 @@ export async function PUT(
|
|||
|
||||
const id = (await context.params)?.id;
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Missing ID in request",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
if (!body.title || typeof body.price !== "number") {
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Invalid data provided",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Invalid data provided",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRecord = await pb.collection("budgetable").update(id, body);
|
||||
|
@ -125,13 +146,16 @@ export async function PUT(
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating data:", error);
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Failed to update data",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Failed to update data",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,17 @@ export async function GET() {
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Failed to fetch data",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Failed to fetch data",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,13 +46,16 @@ export async function POST(req: Request) {
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Error adding data:", error);
|
||||
return Response.json({
|
||||
error: {
|
||||
message: "Failed to add data",
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: "Failed to add data",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ const DeleteDialog = ({ onConfirm }: DeleteDialogProps) => (
|
|||
<DialogTitle>Delete Row</DialogTitle>
|
||||
<DialogHeader>Are you sure you want to delete this row?</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={onConfirm}>Yes, delete</Button>
|
||||
<Button onClick={onConfirm} variant="destructive">
|
||||
Yes, delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
@ -13,7 +13,7 @@ const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
|
|||
href="/"
|
||||
className="text-blue-500 hover:text-blue-600 transition-colors"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
>
|
||||
Budgetable
|
||||
</Link>
|
||||
</h1>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TableRow, TableCell } from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Budgetable } from '@/lib/utils';
|
||||
import type { Budgetable } from "@/lib/utils";
|
||||
|
||||
interface TNewRowProps {
|
||||
newRow: Budgetable;
|
||||
|
|
|
@ -3,8 +3,14 @@ import { TableRow, TableCell } from "@/components/ui/table";
|
|||
import Link from "next/link";
|
||||
import StatusBadge from "@/components/status-badge";
|
||||
import DeleteDialog from "@/components/delete-dialog";
|
||||
import { useState } from 'react';
|
||||
import { Budgetable } from '@/lib/utils';
|
||||
import { useState } from "react";
|
||||
import type { Budgetable } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface TRowProps {
|
||||
row: Budgetable;
|
||||
|
@ -31,7 +37,7 @@ const TRow = ({
|
|||
const [originalRow, setOriginalRow] = useState<Budgetable>(row);
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setOriginalRow({...row});
|
||||
setOriginalRow({ ...row });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -48,7 +54,9 @@ const TRow = ({
|
|||
onChange={(e) =>
|
||||
setData((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === row.id ? { ...item, title: e.target.value } : item,
|
||||
item.id === row.id
|
||||
? { ...item, title: e.target.value }
|
||||
: item,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -94,14 +102,18 @@ const TRow = ({
|
|||
onBlur={() => handleSave(row, originalRow)}
|
||||
/>
|
||||
) : row.link ? (
|
||||
<Link
|
||||
href={row.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
Visit
|
||||
</Link>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="text-blue-500 underline">
|
||||
<Link href={row.link} target="_blank" rel="noopener noreferrer">
|
||||
Visit
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-white bg-black">
|
||||
{row.link.replace(/^https?:\/\//, "")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">No link</span>
|
||||
)}
|
||||
|
@ -125,7 +137,10 @@ const TRow = ({
|
|||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={row.status} toggleStatus={() => toggleStatus(row)} />
|
||||
<StatusBadge
|
||||
status={row.status}
|
||||
toggleStatus={() => toggleStatus(row)}
|
||||
/>
|
||||
</TableCell>
|
||||
{isEditing && (
|
||||
<TableCell>
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "@/components/ui/table";
|
||||
import TRow from "@/components/table-row";
|
||||
import TNewRow from "@/components/table-new-row";
|
||||
import { Budgetable } from '@/lib/utils';
|
||||
import type { Budgetable } from "@/lib/utils";
|
||||
|
||||
interface TWrapperProps {
|
||||
data: Budgetable[];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ComponentProps } from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import NumberFlow from "@number-flow/react";
|
||||
|
||||
interface TotalDisplayProps {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const TotalDisplay = ({ total }: TotalDisplayProps) => (
|
||||
<div className="mt-4 text-right font-bold text-lg">
|
||||
Total: {total.toLocaleString()}
|
||||
<p className="text-xs ml-1">
|
||||
({Math.round(total * 1.27).toLocaleString()})
|
||||
</p>
|
||||
<div className="mx-2 text-left font-bold text-lg">
|
||||
Total: <NumberFlow value={total} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
32
src/components/ui/tooltip.tsx
Normal file
32
src/components/ui/tooltip.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@ -16,14 +16,14 @@ export interface Budgetable {
|
|||
|
||||
export const areRowsEqual = (row1: Budgetable, row2: Budgetable): boolean => {
|
||||
const normalize = (value: string | number | undefined) =>
|
||||
String(value ?? "").trim();
|
||||
String(value ?? "").trim();
|
||||
|
||||
const areEqual = (field: keyof Budgetable) =>
|
||||
field === "price"
|
||||
? Number(row1[field]) === Number(row2[field])
|
||||
: normalize(row1[field]) === normalize(row2[field]);
|
||||
field === "price"
|
||||
? Number(row1[field]) === Number(row2[field])
|
||||
: normalize(row1[field]) === normalize(row2[field]);
|
||||
|
||||
return ["title", "price", "link", "note", "status"].every(field =>
|
||||
areEqual(field as keyof Budgetable)
|
||||
return ["title", "price", "link", "note", "status"].every((field) =>
|
||||
areEqual(field as keyof Budgetable),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue