From 86863ee61ae1af2d40aefc3e557529793ac00d34 Mon Sep 17 00:00:00 2001 From: Divya Jeevanantham <16463670+divya-jeevanantham@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:57:13 +0200 Subject: [PATCH] Banking App --- .env.example | 25 + .eslintrc.json | 3 + .gitignore | 42 + .vscode/launch.json | 32 + README.md | 36 + app/(auth)/layout.tsx | 24 + app/(auth)/sign-in/page.tsx | 11 + app/(auth)/sign-up/page.tsx | 11 + app/(root)/layout.tsx | 31 + app/(root)/my-banks/page.tsx | 40 + app/(root)/page.tsx | 57 + app/(root)/payment-transfer/page.tsx | 31 + app/(root)/transaction-history/page.tsx | 75 + app/api/sentry-example-api/route.js | 9 + app/global-error.jsx | 19 + app/globals.css | 378 ++ app/layout.tsx | 32 + components.json | 17 + components/AnimatedCounter.tsx | 18 + components/AuthForm.tsx | 176 + components/BankCard.tsx | 68 + components/BankDropdown.tsx | 84 + components/BankInfo.tsx | 74 + components/BankTabItem.tsx | 37 + components/Category.tsx | 38 + components/Copy.tsx | 65 + components/CustomInput.tsx | 45 + components/DoughnutChart.tsx | 38 + components/Footer.tsx | 39 + components/HeaderBox.tsx | 17 + components/MobileNav.tsx | 84 + components/Pagination.tsx | 64 + components/PaymentTransferForm.tsx | 255 + components/PlaidLink.tsx | 74 + components/RecentTransactions.tsx | 76 + components/RightSidebar.tsx | 84 + components/Sidebar.tsx | 60 + components/TotalBalanceBox.tsx | 31 + components/TransactionsTable.tsx | 91 + components/ui/button.tsx | 56 + components/ui/form.tsx | 176 + components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/progress.tsx | 28 + components/ui/select.tsx | 160 + components/ui/sheet.tsx | 140 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 24 + constants/index.ts | 143 + lib/actions/bank.actions.ts | 185 + lib/actions/dwolla.actions.ts | 114 + lib/actions/transaction.actions.ts | 61 + lib/actions/user.actions.ts | 295 ++ lib/appwrite.ts | 44 + lib/plaid.ts | 13 + lib/utils.ts | 211 + next.config.mjs | 47 + package-lock.json | 6476 +++++++++++++++++++++++ package.json | 49 + postcss.config.mjs | 8 + public/icons/Lines.svg | 60 + public/icons/Paypass.svg | 10 + public/icons/a-coffee.svg | 4 + public/icons/arrow-down.svg | 3 + public/icons/arrow-left.svg | 3 + public/icons/arrow-right.svg | 3 + public/icons/arrow-up.svg | 3 + public/icons/auth-image.svg | 3 + public/icons/bank-transfer.svg | 3 + public/icons/coins.svg | 10 + public/icons/connect-bank.svg | 8 + public/icons/credit-card.svg | 9 + public/icons/deposit.svg | 3 + public/icons/dollar-circle.svg | 5 + public/icons/dollar.svg | 3 + public/icons/edit.svg | 3 + public/icons/figma.svg | 9 + public/icons/filter-lines.svg | 3 + public/icons/fresh-fv.svg | 4 + public/icons/google.svg | 13 + public/icons/gradient-mesh.svg | 9 + public/icons/hamburger.svg | 10 + public/icons/home.svg | 3 + public/icons/jsm.svg | 9 + public/icons/lines.png | Bin 0 -> 15369 bytes public/icons/loader.svg | 26 + public/icons/logo.svg | 58 + public/icons/logout.svg | 3 + public/icons/mastercard.svg | 6 + public/icons/money-send.svg | 7 + public/icons/monitor.svg | 3 + public/icons/payment-transfer.svg | 7 + public/icons/plus.svg | 3 + public/icons/search.svg | 3 + public/icons/shopping-bag.svg | 3 + public/icons/spotify.svg | 9 + public/icons/stripe.svg | 9 + public/icons/tbfBakery.svg | 4 + public/icons/transaction.svg | 8 + public/icons/visa.svg | 4 + sentry.client.config.ts | 30 + sentry.edge.config.ts | 16 + sentry.server.config.ts | 19 + tailwind.config.ts | 109 + tsconfig.json | 26 + types/index.d.ts | 330 ++ 107 files changed, 11720 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 app/(auth)/layout.tsx create mode 100644 app/(auth)/sign-in/page.tsx create mode 100644 app/(auth)/sign-up/page.tsx create mode 100644 app/(root)/layout.tsx create mode 100644 app/(root)/my-banks/page.tsx create mode 100644 app/(root)/page.tsx create mode 100644 app/(root)/payment-transfer/page.tsx create mode 100644 app/(root)/transaction-history/page.tsx create mode 100644 app/api/sentry-example-api/route.js create mode 100644 app/global-error.jsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 components.json create mode 100644 components/AnimatedCounter.tsx create mode 100644 components/AuthForm.tsx create mode 100644 components/BankCard.tsx create mode 100644 components/BankDropdown.tsx create mode 100644 components/BankInfo.tsx create mode 100644 components/BankTabItem.tsx create mode 100644 components/Category.tsx create mode 100644 components/Copy.tsx create mode 100644 components/CustomInput.tsx create mode 100644 components/DoughnutChart.tsx create mode 100644 components/Footer.tsx create mode 100644 components/HeaderBox.tsx create mode 100644 components/MobileNav.tsx create mode 100644 components/Pagination.tsx create mode 100644 components/PaymentTransferForm.tsx create mode 100644 components/PlaidLink.tsx create mode 100644 components/RecentTransactions.tsx create mode 100644 components/RightSidebar.tsx create mode 100644 components/Sidebar.tsx create mode 100644 components/TotalBalanceBox.tsx create mode 100644 components/TransactionsTable.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 constants/index.ts create mode 100644 lib/actions/bank.actions.ts create mode 100644 lib/actions/dwolla.actions.ts create mode 100644 lib/actions/transaction.actions.ts create mode 100644 lib/actions/user.actions.ts create mode 100644 lib/appwrite.ts create mode 100644 lib/plaid.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/icons/Lines.svg create mode 100644 public/icons/Paypass.svg create mode 100644 public/icons/a-coffee.svg create mode 100644 public/icons/arrow-down.svg create mode 100644 public/icons/arrow-left.svg create mode 100644 public/icons/arrow-right.svg create mode 100644 public/icons/arrow-up.svg create mode 100644 public/icons/auth-image.svg create mode 100644 public/icons/bank-transfer.svg create mode 100644 public/icons/coins.svg create mode 100644 public/icons/connect-bank.svg create mode 100644 public/icons/credit-card.svg create mode 100644 public/icons/deposit.svg create mode 100644 public/icons/dollar-circle.svg create mode 100644 public/icons/dollar.svg create mode 100644 public/icons/edit.svg create mode 100644 public/icons/figma.svg create mode 100644 public/icons/filter-lines.svg create mode 100644 public/icons/fresh-fv.svg create mode 100644 public/icons/google.svg create mode 100644 public/icons/gradient-mesh.svg create mode 100644 public/icons/hamburger.svg create mode 100644 public/icons/home.svg create mode 100644 public/icons/jsm.svg create mode 100644 public/icons/lines.png create mode 100644 public/icons/loader.svg create mode 100644 public/icons/logo.svg create mode 100644 public/icons/logout.svg create mode 100644 public/icons/mastercard.svg create mode 100644 public/icons/money-send.svg create mode 100644 public/icons/monitor.svg create mode 100644 public/icons/payment-transfer.svg create mode 100644 public/icons/plus.svg create mode 100644 public/icons/search.svg create mode 100644 public/icons/shopping-bag.svg create mode 100644 public/icons/spotify.svg create mode 100644 public/icons/stripe.svg create mode 100644 public/icons/tbfBakery.svg create mode 100644 public/icons/transaction.svg create mode 100644 public/icons/visa.svg create mode 100644 sentry.client.config.ts create mode 100644 sentry.edge.config.ts create mode 100644 sentry.server.config.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types/index.d.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..59b0c0e --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +#NEXT +NEXT_PUBLIC_SITE_URL=http://localhost:3000 + +#APPWRITE +NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +NEXT_PUBLIC_APPWRITE_PROJECT= +APPWRITE_DATABASE_ID= +APPWRITE_USER_COLLECTION_ID= +APPWRITE_ITEM_COLLECTION_ID= +APPWRITE_BANK_COLLECTION_ID= +APPWRITE_TRANSACTION_COLLECTION_ID= +NEXT_APPWRITE_KEY= + +#PLAID +PLAID_CLIENT_ID= +PLAID_SECRET= +PLAID_ENV= +PLAID_PRODUCTS= +PLAID_COUNTRY_CODES= + +#DWOLLA +DWOLLA_KEY= +DWOLLA_SECRET= +DWOLLA_BASE_URL= +DWOLLA_ENV= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d85df75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.sentryclirc + +# Environment file +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0f38326 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": ["--inspect"], + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithChrome", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } + } + ] + } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cbc820 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. \ No newline at end of file diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..e427477 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,24 @@ +import Image from "next/image"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {children} +
+
+ Auth image +
+
+
+ ); +} diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx new file mode 100644 index 0000000..36be981 --- /dev/null +++ b/app/(auth)/sign-in/page.tsx @@ -0,0 +1,11 @@ +import AuthForm from '@/components/AuthForm' + +const SignIn = () => { + return ( +
+ +
+ ) +} + +export default SignIn \ No newline at end of file diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx new file mode 100644 index 0000000..6b1f740 --- /dev/null +++ b/app/(auth)/sign-up/page.tsx @@ -0,0 +1,11 @@ +import AuthForm from '@/components/AuthForm' + +const SignUp = async () => { + return ( +
+ +
+ ) +} + +export default SignUp \ No newline at end of file diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx new file mode 100644 index 0000000..cd7a222 --- /dev/null +++ b/app/(root)/layout.tsx @@ -0,0 +1,31 @@ +import MobileNav from "@/components/MobileNav"; +import Sidebar from "@/components/Sidebar"; +import { getLoggedInUser } from "@/lib/actions/user.actions"; +import Image from "next/image"; +import { redirect } from "next/navigation"; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const loggedIn = await getLoggedInUser(); + + if(!loggedIn) redirect('/sign-in') + + return ( +
+ + +
+
+ logo +
+ +
+
+ {children} +
+
+ ); +} diff --git a/app/(root)/my-banks/page.tsx b/app/(root)/my-banks/page.tsx new file mode 100644 index 0000000..bac7c2c --- /dev/null +++ b/app/(root)/my-banks/page.tsx @@ -0,0 +1,40 @@ +import BankCard from '@/components/BankCard'; +import HeaderBox from '@/components/HeaderBox' +import { getAccounts } from '@/lib/actions/bank.actions'; +import { getLoggedInUser } from '@/lib/actions/user.actions'; +import React from 'react' + +const MyBanks = async () => { + const loggedIn = await getLoggedInUser(); + const accounts = await getAccounts({ + userId: loggedIn.$id + }) + + return ( +
+
+ + +
+

+ Your cards +

+
+ {accounts && accounts.data.map((a: Account) => ( + + ))} +
+
+
+
+ ) +} + +export default MyBanks \ No newline at end of file diff --git a/app/(root)/page.tsx b/app/(root)/page.tsx new file mode 100644 index 0000000..f2e45cd --- /dev/null +++ b/app/(root)/page.tsx @@ -0,0 +1,57 @@ +import HeaderBox from '@/components/HeaderBox' +import RecentTransactions from '@/components/RecentTransactions'; +import RightSidebar from '@/components/RightSidebar'; +import TotalBalanceBox from '@/components/TotalBalanceBox'; +import { getAccount, getAccounts } from '@/lib/actions/bank.actions'; +import { getLoggedInUser } from '@/lib/actions/user.actions'; + +const Home = async ({ searchParams: { id, page } }: SearchParamProps) => { + const currentPage = Number(page as string) || 1; + const loggedIn = await getLoggedInUser(); + const accounts = await getAccounts({ + userId: loggedIn?.$id + }) + + if(!accounts) return; + + const accountsData = accounts?.data; + const appwriteItemId = (id as string) || accountsData[0]?.appwriteItemId; + + const account = await getAccount({ appwriteItemId }) + + return ( +
+
+
+ + + +
+ + +
+ + +
+ ) +} + +export default Home \ No newline at end of file diff --git a/app/(root)/payment-transfer/page.tsx b/app/(root)/payment-transfer/page.tsx new file mode 100644 index 0000000..b11c510 --- /dev/null +++ b/app/(root)/payment-transfer/page.tsx @@ -0,0 +1,31 @@ +import HeaderBox from '@/components/HeaderBox' +import PaymentTransferForm from '@/components/PaymentTransferForm' +import { getAccounts } from '@/lib/actions/bank.actions'; +import { getLoggedInUser } from '@/lib/actions/user.actions'; +import React from 'react' + +const Transfer = async () => { + const loggedIn = await getLoggedInUser(); + const accounts = await getAccounts({ + userId: loggedIn.$id + }) + + if(!accounts) return; + + const accountsData = accounts?.data; + + return ( +
+ + +
+ +
+
+ ) +} + +export default Transfer \ No newline at end of file diff --git a/app/(root)/transaction-history/page.tsx b/app/(root)/transaction-history/page.tsx new file mode 100644 index 0000000..d8a16a4 --- /dev/null +++ b/app/(root)/transaction-history/page.tsx @@ -0,0 +1,75 @@ +import HeaderBox from '@/components/HeaderBox' +import { Pagination } from '@/components/Pagination'; +import TransactionsTable from '@/components/TransactionsTable'; +import { getAccount, getAccounts } from '@/lib/actions/bank.actions'; +import { getLoggedInUser } from '@/lib/actions/user.actions'; +import { formatAmount } from '@/lib/utils'; +import React from 'react' + +const TransactionHistory = async ({ searchParams: { id, page }}:SearchParamProps) => { + const currentPage = Number(page as string) || 1; + const loggedIn = await getLoggedInUser(); + const accounts = await getAccounts({ + userId: loggedIn.$id + }) + + if(!accounts) return; + + const accountsData = accounts?.data; + const appwriteItemId = (id as string) || accountsData[0]?.appwriteItemId; + + const account = await getAccount({ appwriteItemId }) + + +const rowsPerPage = 10; +const totalPages = Math.ceil(account?.transactions.length / rowsPerPage); + +const indexOfLastTransaction = currentPage * rowsPerPage; +const indexOfFirstTransaction = indexOfLastTransaction - rowsPerPage; + +const currentTransactions = account?.transactions.slice( + indexOfFirstTransaction, indexOfLastTransaction +) + return ( +
+
+ +
+ +
+
+
+

{account?.data.name}

+

+ {account?.data.officialName} +

+

+ ●●●● ●●●● ●●●● {account?.data.mask} +

+
+ +
+

Current balance

+

{formatAmount(account?.data.currentBalance)}

+
+
+ +
+ + {totalPages > 1 && ( +
+ +
+ )} +
+
+
+ ) +} + +export default TransactionHistory \ No newline at end of file diff --git a/app/api/sentry-example-api/route.js b/app/api/sentry-example-api/route.js new file mode 100644 index 0000000..f486f3d --- /dev/null +++ b/app/api/sentry-example-api/route.js @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +// A faulty API route to test Sentry's error monitoring +export function GET() { + throw new Error("Sentry Example API Route Error"); + return NextResponse.json({ data: "Testing Sentry Error..." }); +} diff --git a/app/global-error.jsx b/app/global-error.jsx new file mode 100644 index 0000000..2e6130a --- /dev/null +++ b/app/global-error.jsx @@ -0,0 +1,19 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import Error from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..7c41a72 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,378 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} +.glassmorphism { + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.custom-scrollbar::-webkit-scrollbar { + width: 3px; + height: 3px; + border-radius: 2px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: #dddddd; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: #5c5c7b; + border-radius: 50px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #7878a3; +} + +@layer utilities { + .input-class { + @apply text-16 placeholder:text-16 rounded-lg border border-gray-300 text-gray-900 placeholder:text-gray-500; + } + + .sheet-content button { + @apply focus:ring-0 focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important; + } + + .text14_padding10 { + @apply text-14 px-4 py-2.5 font-semibold; + } + + .flex-center { + @apply flex items-center justify-center; + } + + .header-2 { + @apply text-18 font-semibold text-gray-900; + } + + .text-10 { + @apply text-[10px] leading-[14px]; + } + + .text-12 { + @apply text-[12px] leading-[16px]; + } + + .text-14 { + @apply text-[14px] leading-[20px]; + } + + .text-16 { + @apply text-[16px] leading-[24px]; + } + + .text-18 { + @apply text-[18px] leading-[22px]; + } + + .text-20 { + @apply text-[20px] leading-[24px]; + } + + .text-24 { + @apply text-[24px] leading-[30px]; + } + + .text-26 { + @apply text-[26px] leading-[32px]; + } + + .text-30 { + @apply text-[30px] leading-[38px]; + } + + .text-36 { + @apply text-[36px] leading-[44px]; + } + + /* Home */ + .home { + @apply no-scrollbar flex w-full flex-row max-xl:max-h-screen max-xl:overflow-y-scroll; + } + + .home-content { + @apply no-scrollbar flex w-full flex-1 flex-col gap-8 px-5 sm:px-8 py-7 lg:py-12 xl:max-h-screen xl:overflow-y-scroll; + } + + .home-header { + @apply flex flex-col justify-between gap-8; + } + + .total-balance { + @apply flex w-full items-center gap-4 rounded-xl border border-gray-200 p-4 shadow-chart sm:gap-6 sm:p-6; + } + + .total-balance-chart { + @apply flex size-full max-w-[100px] items-center sm:max-w-[120px]; + } + + .total-balance-label { + @apply text-14 font-medium text-gray-600; + } + + .total-balance-amount { + @apply text-24 lg:text-30 flex-1 font-semibold text-gray-900; + } + + .recent-transactions { + @apply flex w-full flex-col gap-6; + } + + .view-all-btn { + @apply text-14 rounded-lg border border-gray-300 px-4 py-2.5 font-semibold text-gray-700; + } + + .recent-transactions { + @apply flex w-full flex-col gap-6; + } + + .recent-transactions-label { + @apply text-20 md:text-24 font-semibold text-gray-900; + } + + .recent-transactions-tablist { + @apply custom-scrollbar mb-8 flex w-full flex-nowrap; + } + + /* Right sidebar */ + .right-sidebar { + @apply no-scrollbar hidden h-screen max-h-screen flex-col border-l border-gray-200 xl:flex w-[355px] xl:overflow-y-scroll !important; + } + + .profile-banner { + @apply h-[120px] w-full bg-gradient-mesh bg-cover bg-no-repeat; + } + + .profile { + @apply relative flex px-6 max-xl:justify-center; + } + + .profile-img { + @apply flex-center absolute -top-8 size-24 rounded-full bg-gray-100 border-8 border-white p-2 shadow-profile; + } + + .profile-details { + @apply flex flex-col pt-24; + } + + .profile-name { + @apply text-24 font-semibold text-gray-900; + } + + .profile-email { + @apply text-16 font-normal text-gray-600; + } + + .banks { + @apply flex flex-col justify-between gap-8 px-6 py-8; + } + + /* My Banks */ + .my-banks { + @apply flex h-screen max-h-screen w-full flex-col gap-8 bg-gray-25 p-8 xl:py-12; + } + + /* My Banks */ + .transactions { + @apply flex max-h-screen w-full flex-col gap-8 overflow-y-scroll bg-gray-25 p-8 xl:py-12; + } + + .transactions-header { + @apply flex w-full flex-col items-start justify-between gap-8 md:flex-row; + } + + .transactions-account { + @apply flex flex-col justify-between gap-4 rounded-lg border-y bg-blue-600 px-4 py-5 md:flex-row; + } + + .transactions-account-balance { + @apply flex-center flex-col gap-2 rounded-md bg-blue-25/20 px-4 py-2 text-white; + } + + .header-box { + @apply flex flex-col gap-1; + } + + .header-box-title { + @apply text-24 lg:text-30 font-semibold text-gray-900; + } + + .header-box-subtext { + @apply text-14 lg:text-16 font-normal text-gray-600; + } + + /* Bank Card */ + .bank-card { + @apply relative flex h-[190px] w-full max-w-[320px] justify-between rounded-[20px] border border-white bg-bank-gradient shadow-creditCard backdrop-blur-[6px]; + } + + .bank-card_content { + @apply relative z-10 flex size-full max-w-[228px] flex-col justify-between rounded-l-[20px] bg-gray-700 bg-bank-gradient px-5 pb-4 pt-5; + } + + .bank-card_icon { + @apply flex size-full flex-1 flex-col items-end justify-between rounded-r-[20px] bg-bank-gradient bg-cover bg-center bg-no-repeat py-5 pr-5; + } + + /* Bank Info */ + .bank-info { + @apply gap-[18px] flex p-4 transition-all border bg-blue-25 border-transparent; + } + + /* Category Badge */ + .category-badge { + @apply flex-center truncate w-fit gap-1 rounded-2xl border-[1.5px] py-[2px] pl-1.5 pr-2; + } + + .banktab-item { + @apply gap-[18px] border-b-2 flex px-2 sm:px-4 py-2 transition-all; + } + + /* Mobile nav */ + .mobilenav-sheet { + @apply flex h-[calc(100vh-72px)] flex-col justify-between overflow-y-auto; + } + + .mobilenav-sheet_close { + @apply flex gap-3 items-center p-4 rounded-lg w-full max-w-60; + } + + /* PlaidLink */ + .plaidlink-primary { + @apply text-16 rounded-lg border border-bankGradient bg-bank-gradient font-semibold text-white shadow-form; + } + + .plaidlink-ghost { + @apply flex cursor-pointer items-center justify-center gap-3 rounded-lg px-3 py-7 hover:bg-white lg:justify-start; + } + + .plaidlink-default { + @apply flex !justify-start cursor-pointer gap-3 rounded-lg !bg-transparent flex-row; + } + + /* Auth */ + .auth-asset { + @apply flex h-screen w-full sticky top-0 items-center justify-end bg-sky-1 max-lg:hidden; + } + + /* Auth Form */ + .auth-form { + @apply flex min-h-screen w-full max-w-[420px] flex-col justify-center gap-5 py-10 md:gap-8; + } + + .form-item { + @apply flex flex-col gap-1.5; + } + + .form-label { + @apply text-14 w-full max-w-[280px] font-medium text-gray-700; + } + + .form-message { + @apply text-12 text-red-500; + } + + .form-btn { + @apply text-16 rounded-lg border border-bankGradient bg-bank-gradient font-semibold text-white shadow-form; + } + + .form-link { + @apply text-14 cursor-pointer font-medium text-bankGradient; + } + + /* Payment Transfer */ + .payment-transfer { + @apply no-scrollbar flex flex-col overflow-y-scroll bg-gray-25 p-8 md:max-h-screen xl:py-12; + } + + .payment-transfer_form-item { + @apply flex w-full max-w-[850px] flex-col gap-3 md:flex-row lg:gap-8; + } + + .payment-transfer_form-content { + @apply flex w-full max-w-[280px] flex-col gap-2; + } + + .payment-transfer_form-details { + @apply flex flex-col gap-1 border-t border-gray-200 pb-5 pt-6; + } + + .payment-transfer_btn-box { + @apply mt-5 flex w-full max-w-[850px] gap-3 border-gray-200 py-5; + } + + .payment-transfer_btn { + @apply text-14 w-full bg-bank-gradient font-semibold text-white shadow-form !important; + } + + /* Root Layout */ + .root-layout { + @apply flex h-16 items-center justify-between p-5 shadow-creditCard sm:p-8 md:hidden; + } + + /* Bank Info */ + .bank-info_content { + @apply flex flex-1 items-center justify-between gap-2 overflow-hidden; + } + + /* Footer */ + .footer { + @apply flex cursor-pointer items-center justify-between gap-2 py-6; + } + + .footer_name { + @apply flex size-10 items-center justify-center rounded-full bg-gray-200 max-xl:hidden; + } + + .footer_email { + @apply flex flex-1 flex-col justify-center max-xl:hidden; + } + + .footer_name-mobile { + @apply flex size-10 items-center justify-center rounded-full bg-gray-200; + } + + .footer_email-mobile { + @apply flex flex-1 flex-col justify-center; + } + + .footer_image { + @apply relative size-5 max-xl:w-full max-xl:flex max-xl:justify-center max-xl:items-center; + } + + /* Sidebar */ + .sidebar { + @apply sticky left-0 top-0 flex h-screen w-fit flex-col justify-between border-r border-gray-200 bg-white pt-8 text-white max-md:hidden sm:p-4 xl:p-6 2xl:w-[355px]; + } + + .sidebar-logo { + @apply 2xl:text-26 font-ibm-plex-serif text-[26px] font-bold text-black-1 max-xl:hidden; + } + + .sidebar-link { + @apply flex gap-3 items-center py-1 md:p-3 2xl:p-4 rounded-lg justify-center xl:justify-start; + } + + .sidebar-label { + @apply text-16 font-semibold text-black-2 max-xl:hidden; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..a24ccc2 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,32 @@ +export const dynamic = 'force-dynamic' + +import type { Metadata } from "next"; +import { Inter, IBM_Plex_Serif } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"], variable: '--font-inter' }); +const ibmPlexSerif = IBM_Plex_Serif({ + subsets: ['latin'], + weight: ['400', '700'], + variable: '--font-ibm-plex-serif' +}) + +export const metadata: Metadata = { + title: "EzBank", + description: "EzBank is a modern banking platform for everyone.", + icons: { + icon: '/icons/logo.svg' + } +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..15f2b02 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/AnimatedCounter.tsx b/components/AnimatedCounter.tsx new file mode 100644 index 0000000..c2e6c7c --- /dev/null +++ b/components/AnimatedCounter.tsx @@ -0,0 +1,18 @@ +'use client'; + +import CountUp from 'react-countup'; + +const AnimatedCounter = ({ amount }: { amount: number }) => { + return ( +
+ +
+ ) +} + +export default AnimatedCounter \ No newline at end of file diff --git a/components/AuthForm.tsx b/components/AuthForm.tsx new file mode 100644 index 0000000..6e88fdf --- /dev/null +++ b/components/AuthForm.tsx @@ -0,0 +1,176 @@ +'use client'; + +import Image from 'next/image' +import Link from 'next/link' +import React, { useState } from 'react' + +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import CustomInput from './CustomInput'; +import { authFormSchema } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { getLoggedInUser, signIn, signUp } from '@/lib/actions/user.actions'; +import PlaidLink from './PlaidLink'; + +const AuthForm = ({ type }: { type: string }) => { + const router = useRouter(); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const formSchema = authFormSchema(type); + + // 1. Define your form. + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: '' + }, + }) + + // 2. Define a submit handler. + const onSubmit = async (data: z.infer) => { + setIsLoading(true); + + try { + // Sign up with Appwrite & create plaid token + + if(type === 'sign-up') { + const userData = { + firstName: data.firstName!, + lastName: data.lastName!, + address1: data.address1!, + city: data.city!, + state: data.state!, + postalCode: data.postalCode!, + dateOfBirth: data.dateOfBirth!, + ssn: data.ssn!, + email: data.email, + password: data.password + } + + const newUser = await signUp(userData); + + setUser(newUser); + } + + if(type === 'sign-in') { + const response = await signIn({ + email: data.email, + password: data.password, + }) + + if(response) router.push('/') + } + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+ + EzBank logo +

EzBank

+ + +
+

+ {user + ? 'Link Account' + : type === 'sign-in' + ? 'Sign In' + : 'Sign Up' + } +

+ {user + ? 'Link your account to get started' + : 'Please enter your details' + } +

+

+
+
+ {user ? ( +
+ +
+ ): ( + <> +
+ + {type === 'sign-up' && ( + <> +
+ + +
+ + +
+ + +
+
+ + +
+ + )} + + + + + +
+ +
+ + + +
+

+ {type === 'sign-in' + ? "Don't have an account?" + : "Already have an account?"} +

+ + {type === 'sign-in' ? 'Sign up' : 'Sign in'} + +
+ + )} +
+ ) +} + +export default AuthForm \ No newline at end of file diff --git a/components/BankCard.tsx b/components/BankCard.tsx new file mode 100644 index 0000000..0122f4d --- /dev/null +++ b/components/BankCard.tsx @@ -0,0 +1,68 @@ +import { formatAmount } from '@/lib/utils' +import Image from 'next/image' +import Link from 'next/link' +import React from 'react' +import Copy from './Copy' + +const BankCard = ({ account, userName, showBalance = true }: CreditCardProps) => { + + console.log(account); + return ( +
+ +
+
+

+ {account.name} +

+

+ {formatAmount(account.currentBalance)} +

+
+ +
+
+

+ {userName} +

+

+ ●● / ●● +

+
+

+ ●●●● ●●●● ●●●● {account?.mask} +

+
+
+ +
+ pay + mastercard +
+ + lines + + + {showBalance && } +
+ ) +} + +export default BankCard \ No newline at end of file diff --git a/components/BankDropdown.tsx b/components/BankDropdown.tsx new file mode 100644 index 0000000..6263f42 --- /dev/null +++ b/components/BankDropdown.tsx @@ -0,0 +1,84 @@ +"use client"; + +import Image from "next/image"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useState } from "react"; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, +} from "@/components/ui/select"; +import { formUrlQuery, formatAmount } from "@/lib/utils"; + +export const BankDropdown = ({ + accounts = [], + setValue, + otherStyles, +}: BankDropdownProps) => { + const searchParams = useSearchParams(); + const router = useRouter(); + const [selected, setSeclected] = useState(accounts[0]); + + const handleBankChange = (id: string) => { + const account = accounts.find((account) => account.appwriteItemId === id)!; + + setSeclected(account); + const newUrl = formUrlQuery({ + params: searchParams.toString(), + key: "id", + value: id, + }); + router.push(newUrl, { scroll: false }); + + if (setValue) { + setValue("senderBank", id); + } + }; + + return ( + + ); +}; diff --git a/components/BankInfo.tsx b/components/BankInfo.tsx new file mode 100644 index 0000000..014d223 --- /dev/null +++ b/components/BankInfo.tsx @@ -0,0 +1,74 @@ +"use client"; + +import Image from "next/image"; +import { useSearchParams, useRouter } from "next/navigation"; + +import { + cn, + formUrlQuery, + formatAmount, + getAccountTypeColors, +} from "@/lib/utils"; + +const BankInfo = ({ account, appwriteItemId, type }: BankInfoProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const isActive = appwriteItemId === account?.appwriteItemId; + + const handleBankChange = () => { + const newUrl = formUrlQuery({ + params: searchParams.toString(), + key: "id", + value: account?.appwriteItemId, + }); + router.push(newUrl, { scroll: false }); + }; + + const colors = getAccountTypeColors(account?.type as AccountTypes); + + return ( +
+
+ {account.subtype} +
+
+
+

+ {account.name} +

+ {type === "full" && ( +

+ {account.subtype} +

+ )} +
+ +

+ {formatAmount(account.currentBalance)} +

+
+
+ ); +}; + +export default BankInfo; diff --git a/components/BankTabItem.tsx b/components/BankTabItem.tsx new file mode 100644 index 0000000..323bceb --- /dev/null +++ b/components/BankTabItem.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useSearchParams, useRouter } from "next/navigation"; + +import { cn, formUrlQuery } from "@/lib/utils"; + +export const BankTabItem = ({ account, appwriteItemId }: BankTabItemProps) => { + const searchParams = useSearchParams(); + const router = useRouter(); + const isActive = appwriteItemId === account?.appwriteItemId; + + const handleBankChange = () => { + const newUrl = formUrlQuery({ + params: searchParams.toString(), + key: "id", + value: account?.appwriteItemId, + }); + router.push(newUrl, { scroll: false }); + }; + + return ( +
+

+ {account.name} +

+
+ ); +}; diff --git a/components/Category.tsx b/components/Category.tsx new file mode 100644 index 0000000..5e6452e --- /dev/null +++ b/components/Category.tsx @@ -0,0 +1,38 @@ +import Image from "next/image"; + +import { topCategoryStyles } from "@/constants"; +import { cn } from "@/lib/utils"; + +import { Progress } from "./ui/progress"; + +const Category = ({ category }: CategoryProps) => { + const { + bg, + circleBg, + text: { main, count }, + progress: { bg: progressBg, indicator }, + icon, + } = topCategoryStyles[category.name as keyof typeof topCategoryStyles] || + topCategoryStyles.default; + + return ( +
+
+ {category.name} +
+
+
+

{category.name}

+

{category.count}

+
+ +
+
+ ); +}; + +export default Category; \ No newline at end of file diff --git a/components/Copy.tsx b/components/Copy.tsx new file mode 100644 index 0000000..58cbabb --- /dev/null +++ b/components/Copy.tsx @@ -0,0 +1,65 @@ +"use client"; +import { useState } from "react"; + +import { Button } from "./ui/button"; + +const Copy = ({ title }: { title: string }) => { + const [hasCopied, setHasCopied] = useState(false); + + const copyToClipboard = () => { + navigator.clipboard.writeText(title); + setHasCopied(true); + + setTimeout(() => { + setHasCopied(false); + }, 2000); + }; + + return ( + + ); +}; + +export default Copy; diff --git a/components/CustomInput.tsx b/components/CustomInput.tsx new file mode 100644 index 0000000..51cc825 --- /dev/null +++ b/components/CustomInput.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { FormControl, FormField, FormLabel, FormMessage } from './ui/form' +import { Input } from './ui/input' + +import { Control, FieldPath } from 'react-hook-form' +import { z } from 'zod' +import { authFormSchema } from '@/lib/utils' + +const formSchema = authFormSchema('sign-up') + +interface CustomInput { + control: Control>, + name: FieldPath>, + label: string, + placeholder: string +} + +const CustomInput = ({ control, name, label, placeholder }: CustomInput) => { + return ( + ( +
+ + {label} + +
+ + + + +
+
+ )} + /> + ) +} + +export default CustomInput \ No newline at end of file diff --git a/components/DoughnutChart.tsx b/components/DoughnutChart.tsx new file mode 100644 index 0000000..12e7bfa --- /dev/null +++ b/components/DoughnutChart.tsx @@ -0,0 +1,38 @@ +"use client" + +import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; +import { Doughnut } from "react-chartjs-2"; + +ChartJS.register(ArcElement, Tooltip, Legend); + + + +const DoughnutChart = ({ accounts }: DoughnutChartProps) => { + const accountNames = accounts.map((a) => a.name); + const balances = accounts.map((a) => a.currentBalance) + + const data = { + datasets: [ + { + label: 'Banks', + data: balances, + backgroundColor: ['#0747b6', '#2265d8', '#2f91fa'] + } + ], + labels: accountNames + } + + return +} + +export default DoughnutChart \ No newline at end of file diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000..1e700ab --- /dev/null +++ b/components/Footer.tsx @@ -0,0 +1,39 @@ +import { logoutAccount } from '@/lib/actions/user.actions' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import React from 'react' + +const Footer = ({ user, type = 'desktop' }: FooterProps) => { + const router = useRouter(); + + const handleLogOut = async () => { + const loggedOut = await logoutAccount(); + + if(loggedOut) router.push('/sign-in') + } + + return ( +
+
+

+ {user?.firstName[0]} +

+
+ +
+

+ {user?.firstName} +

+

+ {user?.email} +

+
+ +
+ logout +
+
+ ) +} + +export default Footer \ No newline at end of file diff --git a/components/HeaderBox.tsx b/components/HeaderBox.tsx new file mode 100644 index 0000000..cfa2b51 --- /dev/null +++ b/components/HeaderBox.tsx @@ -0,0 +1,17 @@ +const HeaderBox = ({ type = "title", title, subtext, user }: HeaderBoxProps) => { + return ( +
+

+ {title} + {type === 'greeting' && ( + +  {user} + + )} +

+

{subtext}

+
+ ) +} + +export default HeaderBox \ No newline at end of file diff --git a/components/MobileNav.tsx b/components/MobileNav.tsx new file mode 100644 index 0000000..08ffa75 --- /dev/null +++ b/components/MobileNav.tsx @@ -0,0 +1,84 @@ +'use client' + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { sidebarLinks } from "@/constants" +import { cn } from "@/lib/utils" +import Image from "next/image" +import Link from "next/link" +import { usePathname } from "next/navigation" +import Footer from "./Footer" + +const MobileNav = ({ user }: MobileNavProps) => { + const pathname = usePathname(); + + return ( +
+ + + menu + + + + EzBank logo +

EzBank

+ +
+ + + + +
+
+
+
+
+ ) +} + +export default MobileNav \ No newline at end of file diff --git a/components/Pagination.tsx b/components/Pagination.tsx new file mode 100644 index 0000000..df77178 --- /dev/null +++ b/components/Pagination.tsx @@ -0,0 +1,64 @@ +"use client"; + +import Image from "next/image"; +import { useRouter, useSearchParams } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { formUrlQuery } from "@/lib/utils"; + +export const Pagination = ({ page, totalPages }: PaginationProps) => { + const router = useRouter(); + const searchParams = useSearchParams()!; + + const handleNavigation = (type: "prev" | "next") => { + const pageNumber = type === "prev" ? page - 1 : page + 1; + + const newUrl = formUrlQuery({ + params: searchParams.toString(), + key: "page", + value: pageNumber.toString(), + }); + + router.push(newUrl, { scroll: false }); + }; + + return ( +
+ +

+ {page} / {totalPages} +

+ +
+ ); +}; diff --git a/components/PaymentTransferForm.tsx b/components/PaymentTransferForm.tsx new file mode 100644 index 0000000..9652d6e --- /dev/null +++ b/components/PaymentTransferForm.tsx @@ -0,0 +1,255 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { createTransfer } from "@/lib/actions/dwolla.actions"; +import { createTransaction } from "@/lib/actions/transaction.actions"; +import { getBank, getBankByAccountId } from "@/lib/actions/user.actions"; +import { decryptId } from "@/lib/utils"; + +import { BankDropdown } from "./BankDropdown"; +import { Button } from "./ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "./ui/form"; +import { Input } from "./ui/input"; +import { Textarea } from "./ui/textarea"; + +const formSchema = z.object({ + email: z.string().email("Invalid email address"), + name: z.string().min(4, "Transfer note is too short"), + amount: z.string().min(4, "Amount is too short"), + senderBank: z.string().min(4, "Please select a valid bank account"), + shareableId: z.string().min(8, "Please select a valid shareable Id"), +}); + +const PaymentTransferForm = ({ accounts }: PaymentTransferFormProps) => { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + email: "", + amount: "", + senderBank: "", + shareableId: "", + }, + }); + + const submit = async (data: z.infer) => { + setIsLoading(true); + + try { + const receiverAccountId = decryptId(data.shareableId); + const receiverBank = await getBankByAccountId({ + accountId: receiverAccountId, + }); + const senderBank = await getBank({ documentId: data.senderBank }); + + const transferParams = { + sourceFundingSourceUrl: senderBank.fundingSourceUrl, + destinationFundingSourceUrl: receiverBank.fundingSourceUrl, + amount: data.amount, + }; + // create transfer + const transfer = await createTransfer(transferParams); + + // create transfer transaction + if (transfer) { + const transaction = { + name: data.name, + amount: data.amount, + senderId: senderBank.userId.$id, + senderBankId: senderBank.$id, + receiverId: receiverBank.userId.$id, + receiverBankId: receiverBank.$id, + email: data.email, + }; + + const newTransaction = await createTransaction(transaction); + + if (newTransaction) { + form.reset(); + router.push("/"); + } + } + } catch (error) { + console.error("Submitting create transfer request failed: ", error); + } + + setIsLoading(false); + }; + + return ( +
+ + ( + +
+
+ + Select Source Bank + + + Select the bank account you want to transfer funds from + +
+
+ + + + +
+
+
+ )} + /> + + ( + +
+
+ + Transfer Note (Optional) + + + Please provide any additional information or instructions + related to the transfer + +
+
+ +