refactor: update imports to use 'type' for TypeScript types and enhance tooltip functionality

This commit is contained in:
skidoodle 2024-12-31 20:27:49 +01:00
parent 8e37da9463
commit 3d082d3092
Signed by: albert
GPG key ID: A06E3070D7D55BF2
14 changed files with 367 additions and 101 deletions

View file

@ -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>
);
}

View file

@ -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" },
},
);
}
}

View file

@ -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" },
},
);
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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[];

View file

@ -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({

View file

@ -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>
);

View 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 };

View file

@ -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),
);
};
};