From 02043518e47c9abff5dee2b441219110db14d25d Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:10:47 -0600 Subject: [PATCH 01/10] frontend: api extraction --- src/main/frontend/src/lib/api.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/frontend/src/lib/api.ts b/src/main/frontend/src/lib/api.ts index 71d5f3c..c49f1de 100644 --- a/src/main/frontend/src/lib/api.ts +++ b/src/main/frontend/src/lib/api.ts @@ -1,5 +1,5 @@ -export function getLoans() { - return fetch("/api/loans") +export async function getLoans() { + return await fetch("/api/loans") .then((response) => response.json()) .catch((error) => console.error(error)); } @@ -8,4 +8,10 @@ export function getAccounts() { return fetch("/api/accounts") .then((response) => response.json()) .catch((error) => console.error(error)); +} + +export function getAccountLoans(id: string | undefined) { + return fetch(`/api/account/${id}/loans`) + .then((response) => response.json()) + .catch((error) => console.error(error)); } \ No newline at end of file From 2ee7df972fcf0d52ac19997d5eb39c90d420e448 Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:12:44 -0600 Subject: [PATCH 02/10] frontend: create accounts and handle dialog switching --- .../src/components/CreateAccountDialog.tsx | 134 ++++++++++++++++++ .../src/components/CreateLoanDialog.tsx | 28 ++-- .../src/components/DialogSwitcher.tsx | 32 +++++ src/main/frontend/src/routes/AdminTable.tsx | 13 +- src/main/frontend/src/routes/CustomerPage.tsx | 33 +++-- 5 files changed, 205 insertions(+), 35 deletions(-) create mode 100644 src/main/frontend/src/components/CreateAccountDialog.tsx create mode 100644 src/main/frontend/src/components/DialogSwitcher.tsx diff --git a/src/main/frontend/src/components/CreateAccountDialog.tsx b/src/main/frontend/src/components/CreateAccountDialog.tsx new file mode 100644 index 0000000..bcb5fc5 --- /dev/null +++ b/src/main/frontend/src/components/CreateAccountDialog.tsx @@ -0,0 +1,134 @@ +import { + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form.tsx"; +import {Button} from "@/components/ui/button.tsx"; +import {z} from "zod"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {useForm} from "react-hook-form"; +import {Input} from "@/components/ui/input.tsx"; + +const formSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + password: z.string(), +}); + +function CreateAccountDialog({toggle}: { toggle?: () => void }) { + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + firstName: "", + lastName: "", + email: "", + password: "", + }, + }); + + const onSubmit = (data: z.infer) => { + fetch("/api/account", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: data.email, + password: data.password, + username: `${data.firstName} ${data.lastName}`, + }), + }).then((response) => { + if (response.ok) { + form.reset(); + if (toggle) { + toggle(); + } + } else { + console.error(response); + } + }).catch((error) => console.error(error)); + }; + + + return ( +
+ + + New Account + + Create a new account by filling out the form below. + + + ( + + First Name + + + + + + )} + /> + ( + + Last Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + + + + + + ) +} + +export default CreateAccountDialog; \ No newline at end of file diff --git a/src/main/frontend/src/components/CreateLoanDialog.tsx b/src/main/frontend/src/components/CreateLoanDialog.tsx index 51e1bd1..2dd50a3 100644 --- a/src/main/frontend/src/components/CreateLoanDialog.tsx +++ b/src/main/frontend/src/components/CreateLoanDialog.tsx @@ -1,15 +1,12 @@ import { - Dialog, DialogClose, - DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, - DialogTrigger } from "@/components/ui/dialog.tsx"; import {Button} from "@/components/ui/button.tsx"; -import {CalendarIcon, FilePlus2} from "lucide-react"; +import {CalendarIcon} from "lucide-react"; import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.tsx"; import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover.tsx"; import {cn} from "@/lib/utils.ts"; @@ -31,7 +28,7 @@ const formSchema = z.object({ date: z.date(), }); -function CreateLoanDialog() { +function CreateLoanDialog({ toggle }: { toggle?: () => void }) { const [accountPopOverOpen, setAccountPopOverOpen] = useState(false); const form = useForm>({ @@ -80,19 +77,14 @@ function CreateLoanDialog() { }, []); return ( - - - - - - - New Loan - - Create a new loan by filling out the form below. - -
+ + New Loan + + Create a new loan by filling out the form below. + + - @@ -244,8 +236,6 @@ function CreateLoanDialog() { -
-
) } diff --git a/src/main/frontend/src/components/DialogSwitcher.tsx b/src/main/frontend/src/components/DialogSwitcher.tsx new file mode 100644 index 0000000..6d00cdc --- /dev/null +++ b/src/main/frontend/src/components/DialogSwitcher.tsx @@ -0,0 +1,32 @@ +import React, {cloneElement} from "react"; +import { + Dialog, + DialogContent, + DialogTrigger +} from "@/components/ui/dialog.tsx"; +import {Button} from "@/components/ui/button.tsx"; +import {FilePlus2} from "lucide-react"; + + +function DialogSwitcher({ children }: { children: React.ReactNode }) { + const [content, setContent] = React.useState(true); + + const toggleContent = () => { + setContent(prev => !prev); + }; + + const validChildren = React.Children.toArray(children).filter(React.isValidElement); + + return ( + + + + + + {content ? cloneElement(validChildren[0] as React.ReactElement, { toggle: toggleContent }) : cloneElement(validChildren[1] as React.ReactElement, { toggle: toggleContent })} + + + ) +} + +export default DialogSwitcher; \ No newline at end of file diff --git a/src/main/frontend/src/routes/AdminTable.tsx b/src/main/frontend/src/routes/AdminTable.tsx index 0adda12..97afd12 100644 --- a/src/main/frontend/src/routes/AdminTable.tsx +++ b/src/main/frontend/src/routes/AdminTable.tsx @@ -19,6 +19,8 @@ import { import {useNavigate} from "react-router-dom"; import CreateLoanDialog from "@/components/CreateLoanDialog.tsx"; +import DialogSwitcher from "@/components/DialogSwitcher.tsx"; +import CreateAccountDialog from "@/components/CreateAccountDialog.tsx"; function AdminTable() { const [data, setData] = useState([]); @@ -45,7 +47,10 @@ function AdminTable() { <>

Loans

- + + + +
{loading ? (

Loading...

@@ -62,9 +67,11 @@ function AdminTable() { - {data.map((loan) => { + {[...data, ...new Array(10 - data.length)].map((loan) => { + if (!loan) { + return (); + } const date = new Date(loan.date); - console.log(loan); return ( {navigate(`/account/${loan.account?.id}`)}} className={"cursor-pointer"}> {loan.account?.email} diff --git a/src/main/frontend/src/routes/CustomerPage.tsx b/src/main/frontend/src/routes/CustomerPage.tsx index 774f73a..73bb88a 100644 --- a/src/main/frontend/src/routes/CustomerPage.tsx +++ b/src/main/frontend/src/routes/CustomerPage.tsx @@ -1,36 +1,43 @@ import { useParams } from "react-router-dom"; import {useCallback, useEffect, useState} from "react"; +import {getAccountLoans} from "@/lib/api.ts"; function CustomerPage() { - const { id } = useParams(); + const { id } = useParams(); - const [customer, setCustomer] = useState(null); + const [account, setAccount] = useState(null); + const [accountLoans, setAccountLoans] = useState([]); - const getCustomer = useCallback(() => { + const getAccount = useCallback(() => { fetch(`/api/account/${id}`) .then((response) => response.json()) .then((data) => { - setCustomer(data); + setAccount(data); }) .catch((error) => console.error(error)); } , [id]); useEffect(() => { - getCustomer(); + getAccount(); + + getAccountLoans(id) + .then((data) => { + setAccountLoans(data); + }) + .catch((error) => console.error(error)); }, []); return ( -
-

{`Customer: ${id}`}

- {customer ? ( -
-

{JSON.stringify(customer)}

-
+
+

{account?.username}

+

{account?.email}

+

{accountLoans.length > 0 ? ( + accountLoans[0].originAmount ) : ( -

Loading...

- )} + "no loans" + )}

); } From cdcf46e0447721d0cae19c7da0c9c22769ecf573 Mon Sep 17 00:00:00 2001 From: Michael Harlow <94586121+michaelharlow@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:04:34 -0600 Subject: [PATCH 03/10] frontend: various components --- src/main/frontend/src/components/ui/card.tsx | 76 ++++ src/main/frontend/src/components/ui/chart.tsx | 373 ++++++++++++++++++ .../frontend/src/components/ui/separator.tsx | 29 ++ 3 files changed, 478 insertions(+) create mode 100644 src/main/frontend/src/components/ui/card.tsx create mode 100644 src/main/frontend/src/components/ui/chart.tsx create mode 100644 src/main/frontend/src/components/ui/separator.tsx diff --git a/src/main/frontend/src/components/ui/card.tsx b/src/main/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/src/main/frontend/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/main/frontend/src/components/ui/chart.tsx b/src/main/frontend/src/components/ui/chart.tsx new file mode 100644 index 0000000..b6ae1e6 --- /dev/null +++ b/src/main/frontend/src/components/ui/chart.tsx @@ -0,0 +1,373 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import { + NameType, + Payload, + ValueType, +} from "recharts/types/component/DefaultTooltipContent" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +