This commit is contained in:
2024-12-30 17:32:55 +01:00
parent bcfa0f92a6
commit dedb1c0628
16 changed files with 316 additions and 239 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ interface DeleteDialogProps {
onConfirm: () => void;
}
const DeleteDialog: React.FC<DeleteDialogProps> = ({ onConfirm }) => (
const DeleteDialog = ({ onConfirm }: DeleteDialogProps) => (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
+11 -2
View File
@@ -1,13 +1,22 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
interface AppHeaderProps {
isEditing: boolean;
setIsEditing: (value: boolean) => void;
}
const Header: React.FC<AppHeaderProps> = ({ isEditing, setIsEditing }) => (
const Header = ({ isEditing, setIsEditing }: AppHeaderProps) => (
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Budgetable</h1>
<h1 className="text-3xl font-bold">
<Link
href="/"
className="text-blue-500 hover:text-blue-600 transition-colors"
onClick={() => setIsEditing(false)}
>
Budgetable
</Link>
</h1>
<Button variant="ghost" onClick={() => setIsEditing(!isEditing)}>
{isEditing ? "Lock" : "Unlock"} Editing
</Button>
+1 -1
View File
@@ -1,5 +1,5 @@
import { Skeleton } from "@/components/ui/skeleton";
const LoadingSkeleton: React.FC = () => <Skeleton className="h-10 w-full" />;
const LoadingSkeleton = () => <Skeleton className="h-10 w-full" />;
export default LoadingSkeleton;
+2 -2
View File
@@ -5,9 +5,9 @@ interface StatusBadgeProps {
toggleStatus: () => void;
}
const StatusBadge: React.FC<StatusBadgeProps> = ({ status, toggleStatus }) => (
const StatusBadge = ({ status, toggleStatus }: StatusBadgeProps) => (
<Badge
variant={status === "Paid" ? "destructive" : "secondary"}
variant={status === "Paid" ? "paid" : "unpaid"}
className="cursor-pointer"
onClick={toggleStatus}
>
+1 -9
View File
@@ -1,15 +1,7 @@
import { TableRow, TableCell } from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
interface Budgetable {
id: string;
title: string;
price: number;
link: string;
note?: string;
status: "Paid" | "Unpaid";
}
import { Budgetable } from '@/lib/utils';
interface TNewRowProps {
newRow: Budgetable;
+109 -104
View File
@@ -3,15 +3,8 @@ import { TableRow, TableCell } from "@/components/ui/table";
import Link from "next/link";
import StatusBadge from "@/components/status-badge";
import DeleteDialog from "@/components/delete-dialog";
interface Budgetable {
id: string;
title: string;
price: number;
link: string;
note?: string;
status: "Paid" | "Unpaid";
}
import { useState } from 'react';
import { Budgetable } from '@/lib/utils';
interface TRowProps {
row: Budgetable;
@@ -26,7 +19,7 @@ interface TRowProps {
toggleStatus: (row: Budgetable) => Promise<void>;
}
const TRow: React.FC<TRowProps> = ({
const TRow = ({
row,
isEditing,
setData,
@@ -34,101 +27,113 @@ const TRow: React.FC<TRowProps> = ({
handleSave,
handleDeleteRow,
toggleStatus,
}) => (
<TableRow
className={`transition-opacity ${
recentlyUpdatedRowId === row.id ? "blur-sm opacity-50" : ""
}`}
>
<TableCell>
{isEditing ? (
<Input
value={row.title}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, title: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, { ...row })}
/>
) : (
<span>{row.title}</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
type="number"
value={row.price}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id
? { ...item, price: Number.parseFloat(e.target.value) || 0 }
: item,
),
)
}
onBlur={() => handleSave(row, { ...row })}
/>
) : (
<span>{row.price.toLocaleString()} HUF</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
value={row.link}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, link: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, { ...row })}
/>
) : row.link ? (
<Link
href={row.link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Visit
</Link>
) : (
<span className="text-gray-400 italic">No link</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
value={row.note || ""}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, note: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, { ...row })}
/>
) : (
<span>{row.note || "-"}</span>
)}
</TableCell>
<TableCell>
<StatusBadge status={row.status} toggleStatus={() => toggleStatus(row)} />
</TableCell>
{isEditing && (
}: TRowProps) => {
const [originalRow, setOriginalRow] = useState<Budgetable>(row);
const handleInputFocus = () => {
setOriginalRow({...row});
};
return (
<TableRow
className={`transition-opacity ${
recentlyUpdatedRowId === row.id ? "blur-sm opacity-50" : ""
}`}
>
<TableCell>
<DeleteDialog onConfirm={() => handleDeleteRow(row.id)} />
{isEditing ? (
<Input
value={row.title}
onFocus={handleInputFocus}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, title: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, originalRow)}
/>
) : (
<span>{row.title}</span>
)}
</TableCell>
)}
</TableRow>
);
<TableCell>
{isEditing ? (
<Input
type="number"
value={row.price}
onFocus={handleInputFocus}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id
? { ...item, price: Number.parseFloat(e.target.value) || 0 }
: item,
),
)
}
onBlur={() => handleSave(row, originalRow)}
/>
) : (
<span>{row.price.toLocaleString()} HUF</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
value={row.link}
onFocus={handleInputFocus}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, link: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, originalRow)}
/>
) : row.link ? (
<Link
href={row.link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Visit
</Link>
) : (
<span className="text-gray-400 italic">No link</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
value={row.note || ""}
onFocus={handleInputFocus}
onChange={(e) =>
setData((prev) =>
prev.map((item) =>
item.id === row.id ? { ...item, note: e.target.value } : item,
),
)
}
onBlur={() => handleSave(row, originalRow)}
/>
) : (
<span>{row.note || "-"}</span>
)}
</TableCell>
<TableCell>
<StatusBadge status={row.status} toggleStatus={() => toggleStatus(row)} />
</TableCell>
{isEditing && (
<TableCell>
<DeleteDialog onConfirm={() => handleDeleteRow(row.id)} />
</TableCell>
)}
</TableRow>
);
};
export default TRow;
+3 -11
View File
@@ -7,15 +7,7 @@ import {
} from "@/components/ui/table";
import TRow from "@/components/table-row";
import TNewRow from "@/components/table-new-row";
interface Budgetable {
id: string;
title: string;
price: number;
link: string;
note?: string;
status: "Paid" | "Unpaid";
}
import { Budgetable } from '@/lib/utils';
interface TWrapperProps {
data: Budgetable[];
@@ -33,7 +25,7 @@ interface TWrapperProps {
toggleStatus: (row: Budgetable) => Promise<void>;
}
const TWrapper: React.FC<TWrapperProps> = ({
const TWrapper = ({
data,
isEditing,
setData,
@@ -44,7 +36,7 @@ const TWrapper: React.FC<TWrapperProps> = ({
handleAddRow,
handleDeleteRow,
toggleStatus,
}) => (
}: TWrapperProps) => (
<Table>
<TableHeader>
<TableRow>
+2 -2
View File
@@ -1,11 +1,11 @@
"use client";
import type * as React from "react";
import { ComponentProps } from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
}: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+1 -1
View File
@@ -2,7 +2,7 @@ interface TotalDisplayProps {
total: number;
}
const TotalDisplay: React.FC<TotalDisplayProps> = ({ total }) => (
const TotalDisplay = ({ total }: TotalDisplayProps) => (
<div className="mt-4 text-right font-bold text-lg">
Total: {total.toLocaleString()}
<p className="text-xs ml-1">
+2
View File
@@ -15,6 +15,8 @@ const badgeVariants = cva(
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
paid: "border-transparent bg-green-500 text-green-50",
unpaid: "border-transparent bg-red-500 text-red-50",
},
},
defaultVariants: {