From 792f97b5beb473aa4b39e7a39c9034036c81bef2 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 10 Mar 2024 18:24:58 +0100 Subject: [PATCH 1/8] N-FIN-7: fix page title --- src/components/categoryPageClientComponents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/categoryPageClientComponents.tsx b/src/components/categoryPageClientComponents.tsx index 198508d..6de7ec5 100644 --- a/src/components/categoryPageClientComponents.tsx +++ b/src/components/categoryPageClientComponents.tsx @@ -94,7 +94,7 @@ export default function CategoryPageClientContent({categories, onSubmit, onDelet return (
-

Entities

+

Categories

{/* Edit dialog */} From 9a7da676ea19787520f4b5032b7ccaa72043fb7f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 10 Mar 2024 18:25:11 +0100 Subject: [PATCH 2/8] N-FIN-7: add payment form schema --- src/lib/form-schemas/paymentFormSchema.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/lib/form-schemas/paymentFormSchema.ts diff --git a/src/lib/form-schemas/paymentFormSchema.ts b/src/lib/form-schemas/paymentFormSchema.ts new file mode 100644 index 0000000..0f8ede6 --- /dev/null +++ b/src/lib/form-schemas/paymentFormSchema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const paymentFormSchema = z.object({ + id: z.number().positive().optional(), + amount: z.number().positive(), + date: z.date(), + payorId: z.number().positive(), + payeeId: z.number().positive(), + categoryId: z.number().positive().optional(), + note: z.string().max(255).optional(), +}); From 09c8e6f567e4d10d02f797d94bbf9b12202453c6 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 10 Mar 2024 18:27:12 +0100 Subject: [PATCH 3/8] N-FIN-7: add payment server actions --- src/lib/actions/paymentCreateUpdate.ts | 77 ++++++++++++++++++++++++++ src/lib/actions/paymentDelete.ts | 62 +++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/lib/actions/paymentCreateUpdate.ts create mode 100644 src/lib/actions/paymentDelete.ts diff --git a/src/lib/actions/paymentCreateUpdate.ts b/src/lib/actions/paymentCreateUpdate.ts new file mode 100644 index 0000000..d49646d --- /dev/null +++ b/src/lib/actions/paymentCreateUpdate.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; +import { ActionResponse } from '@/lib/types/ActionResponse'; +import { prismaClient } from '@/prisma'; +import { getUser } from '@/auth'; +import { URL_SIGN_IN } from '@/lib/constants'; +import { paymentFormSchema } from '@/lib/form-schemas/paymentFormSchema'; + +export default async function paymentCreateUpdate({ + id, + amount, + date, + payorId, + payeeId, + categoryId, + note, +}: z.infer): Promise { + 'use server'; + + // check that user is logged in + const user = await getUser(); + if (!user) { + return { + type: 'error', + message: 'You must be logged in to create/update a payment.', + redirect: URL_SIGN_IN, + }; + } + + // create/update payment + try { + if (id) { + await prismaClient.payment.update({ + where: { + id: id, + }, + data: { + amount: amount, + date: date, + payorId: payorId, + payeeId: payeeId, + categoryId: categoryId, + note: note, + }, + }, + ); + + // return success + return { + type: 'success', + message: `Payment updated`, + }; + } else { + await prismaClient.payment.create({ + data: { + userId: user.id, + amount: amount, + date: date, + payorId: payorId, + payeeId: payeeId, + categoryId: categoryId, + note: note, + }, + }); + + // return success + return { + type: 'success', + message: `Payment created`, + }; + } + } catch (e) { + return { + type: 'error', + message: 'Failed creating/updating payment', + }; + } +} diff --git a/src/lib/actions/paymentDelete.ts b/src/lib/actions/paymentDelete.ts new file mode 100644 index 0000000..46ee086 --- /dev/null +++ b/src/lib/actions/paymentDelete.ts @@ -0,0 +1,62 @@ +import { ActionResponse } from '@/lib/types/ActionResponse'; +import { prismaClient } from '@/prisma'; +import { getUser } from '@/auth'; +import { URL_SIGN_IN } from '@/lib/constants'; + +export default async function paymentDelete(id: number): Promise { + 'use server'; + + // check that id is a number + if (!id || isNaN(id)) { + return { + type: 'error', + message: 'Invalid payment ID', + }; + } + + // check that user is logged in + const user = await getUser(); + if (!user) { + return { + type: 'error', + message: 'You must be logged in to delete a payment.', + redirect: URL_SIGN_IN, + }; + } + + // check that payment is associated with user + const payment = await prismaClient.payment.findFirst({ + where: { + id: id, + userId: user.id, + }, + }); + if (!payment) { + return { + type: 'error', + message: 'Payment not found', + }; + } + + // delete payment + try { + await prismaClient.payment.delete({ + where: { + id: payment.id, + userId: user.id, + }, + }, + ); + } catch (e) { + return { + type: 'error', + message: 'Failed deleting payment', + }; + } + + // return success + return { + type: 'success', + message: `Payment deleted`, + }; +} From 9d899b29e01be407cf07d4c03bc1b0f655387c28 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 10 Mar 2024 21:11:17 +0100 Subject: [PATCH 4/8] N-FIN-7: fix date format --- src/app/categories/columns.tsx | 7 +++---- src/app/entities/columns.tsx | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/categories/columns.tsx b/src/app/categories/columns.tsx index 66d252b..196bcdd 100644 --- a/src/app/categories/columns.tsx +++ b/src/app/categories/columns.tsx @@ -3,6 +3,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { Category } from '@prisma/client'; import { CellContext, ColumnDefTemplate } from '@tanstack/table-core'; +import { format } from 'date-fns'; export const columns = ( actionCell: ColumnDefTemplate>, @@ -29,16 +30,14 @@ export const columns = ( accessorKey: 'createdAt', header: 'Created at', cell: ({row}) => { - const date = row.getValue('createdAt') as Date; - return date.toDateString(); + return format(row.original.createdAt, 'PPP'); }, }, { accessorKey: 'updatedAt', header: 'Updated at', cell: ({row}) => { - const date = row.getValue('updatedAt') as Date; - return date.toDateString(); + return format(row.original.updatedAt, 'PPP'); }, }, { diff --git a/src/app/entities/columns.tsx b/src/app/entities/columns.tsx index 17ac7d7..a56dc66 100644 --- a/src/app/entities/columns.tsx +++ b/src/app/entities/columns.tsx @@ -3,6 +3,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { Entity } from '@prisma/client'; import { CellContext, ColumnDefTemplate } from '@tanstack/table-core'; +import { format } from 'date-fns'; export const columns = ( actionCell: ColumnDefTemplate>, @@ -21,16 +22,14 @@ export const columns = ( accessorKey: 'createdAt', header: 'Created at', cell: ({row}) => { - const date = row.getValue('createdAt') as Date; - return date.toDateString(); + return format(row.original.createdAt, 'PPP'); }, }, { accessorKey: 'updatedAt', header: 'Updated at', cell: ({row}) => { - const date = row.getValue('updatedAt') as Date; - return date.toDateString(); + return format(row.original.updatedAt, 'PPP'); }, }, { From 756dcb5618a997ce6871d1eee51c65ec37e5f19d Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 11 Mar 2024 00:34:45 +0100 Subject: [PATCH 5/8] N-FIN-7: add required shadcn/ui components --- package-lock.json | 108 ++++++++++++++++++++ package.json | 5 + src/components/ui/calendar.tsx | 67 +++++++++++++ src/components/ui/command.tsx | 157 ++++++++++++++++++++++++++++++ src/components/ui/popover.tsx | 31 ++++++ src/components/ui/scroll-area.tsx | 48 +++++++++ src/components/ui/textarea.tsx | 25 +++++ 7 files changed, 441 insertions(+) create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/textarea.tsx diff --git a/package-lock.json b/package-lock.json index e99aefb..163fe2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,17 +16,22 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.13.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "^1.0.0", + "date-fns": "^3.3.1", "lucia": "^3.0.1", "lucide-react": "^0.350.0", "next": "14.1.3", "next-themes": "^0.2.1", "oslo": "^1.1.3", "react": "^18", + "react-day-picker": "^8.10.0", "react-dom": "^18", "react-hook-form": "^7.51.0", "sonner": "^1.4.3", @@ -1055,6 +1060,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -1188,6 +1230,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", + "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", @@ -2251,6 +2324,19 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2323,6 +2409,15 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4943,6 +5038,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz", + "integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index 8f98fd3..216bd23 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,22 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-table": "^8.13.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "^1.0.0", + "date-fns": "^3.3.1", "lucia": "^3.0.1", "lucide-react": "^0.350.0", "next": "14.1.3", "next-themes": "^0.2.1", "oslo": "^1.1.3", "react": "^18", + "react-day-picker": "^8.10.0", "react-dom": "^18", "react-hook-form": "^7.51.0", "sonner": "^1.4.3", diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..c465642 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,67 @@ +'use client'; + +import * as React from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { DayPicker } from 'react-day-picker'; + +import { cn } from '@/lib/utils'; +import { buttonVariants } from '@/components/ui/button'; + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({...props}) => , + }} + {...props} + /> + ); +} + +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..ec71903 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,157 @@ +'use client'; + +import * as React from 'react'; +import { type DialogProps } from '@radix-ui/react-dialog'; +import { Command as CommandPrimitive } from 'cmdk'; +import { Search } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps { +} + +const CommandDialog = ({children, ...props}: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = 'CommandShortcut'; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..a84f99c --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; + +import { cn } from '@/lib/utils'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, align = 'center', sideOffset = 4, ...props}, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..c7e7693 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +'use client'; + +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; + +import { cn } from '@/lib/utils'; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, children, ...props}, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, orientation = 'vertical', ...props}, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..8313400 --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +export interface TextareaProps + extends React.TextareaHTMLAttributes { +} + +const Textarea = React.forwardRef( + ({className, ...props}, ref) => { + return ( +