[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-73755":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":25,"hasPages":23,"topics":26,"createdAt":10,"pushedAt":10,"updatedAt":28,"readmeContent":29,"aiSummary":30,"trendingCount":16,"starSnapshotCount":16,"syncStatus":17,"lastSyncTime":31,"discoverSource":32},73755,"banking","adrianhajdin\u002Fbanking","adrianhajdin","Horizon is a modern banking platform for everyone.","https:\u002F\u002Fbanking-jet.vercel.app",null,"TypeScript",3145,988,31,72,0,2,3,11,6,30.99,"MIT License",false,"main",true,[27],"nextjs14","2026-06-12 02:03:17","\u003Cdiv align=\"center\">\n  \u003Cbr \u002F>\n    \u003Ca href=\"https:\u002F\u002Fyoutu.be\u002FPuOVqP_cjkE?feature=shared\" target=\"_blank\">\n      \u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fadrianhajdin\u002Fbanking\u002Fassets\u002F151519281\u002F3c03519c-7ebd-4539-b598-49e63d1770b4\" alt=\"Project Banner\">\n    \u003C\u002Fa>\n  \u003Cbr \u002F>\n  \n  \u003Cdiv>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000\" alt=\"nextdotjs\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6\" alt=\"typescript\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4\" alt=\"tailwindcss\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Appwrite-black?style=for-the-badge&logoColor=white&logo=appwrite&color=FD366E\" alt=\"appwrite\" \u002F>\n  \u003C\u002Fdiv>\n\n  \u003Ch3 align=\"center\">A Fintech Bank Application\u003C\u002Fh3>\n\n   \u003Cdiv align=\"center\">\n     Build this project step by step with our detailed tutorial on \u003Ca href=\"https:\u002F\u002Fwww.youtube.com\u002F@javascriptmastery\u002Fvideos\" target=\"_blank\">\u003Cb>JavaScript Mastery\u003C\u002Fb>\u003C\u002Fa> YouTube. Join the JSM family!\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n## 📋 \u003Ca name=\"table\">Table of Contents\u003C\u002Fa>\n\n1. 🤖 [Introduction](#introduction)\n2. ⚙️ [Tech Stack](#tech-stack)\n3. 🔋 [Features](#features)\n4. 🤸 [Quick Start](#quick-start)\n5. 🕸️ [Code Snippets to Copy](#snippets)\n6. 🔗 [Assets](#links)\n7. 🚀 [More](#more)\n\n## 🚨 Tutorial\n\nThis repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, \u003Ca href=\"https:\u002F\u002Fwww.youtube.com\u002F@javascriptmastery\u002Fvideos\" target=\"_blank\">\u003Cb>JavaScript Mastery\u003C\u002Fb>\u003C\u002Fa>. \n\nIf you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!\n\n\u003Ca href=\"https:\u002F\u002Fyoutu.be\u002FPuOVqP_cjkE?feature=shared\" target=\"_blank\">\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002F1736fca5-a031-4854-8c09-bc110e3bc16d\" \u002F>\u003C\u002Fa>\n\n## \u003Ca name=\"introduction\">🤖 Introduction\u003C\u002Fa>\n\nBuilt with Next.js, Horizon is a financial SaaS platform that connects to multiple bank accounts, displays transactions in real-time, allows users to transfer money to other platform users, and manages their finances altogether. \n\nIf you're getting started and need assistance or face any bugs, join our active Discord community with over **34k+** members. It's a place where people help each other out.\n\n\u003Ca href=\"https:\u002F\u002Fdiscord.com\u002Finvite\u002Fn6EdbFJ\" target=\"_blank\">\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002F618f4872-1e10-42da-8213-1d69e486d02e\" \u002F>\u003C\u002Fa>\n\n## \u003Ca name=\"tech-stack\">⚙️ Tech Stack\u003C\u002Fa>\n\n- Next.js\n- TypeScript\n- Appwrite\n- Plaid\n- Dwolla\n- React Hook Form\n- Zod\n- TailwindCSS\n- Chart.js\n- ShadCN\n\n## \u003Ca name=\"features\">🔋 Features\u003C\u002Fa>\n\n👉 **Authentication**: An ultra-secure SSR authentication with proper validations and authorization\n\n👉 **Connect Banks**: Integrates with Plaid for multiple bank account linking\n\n👉 **Home Page**: Shows general overview of user account with total balance from all connected banks, recent transactions, money spent on different categories, etc\n\n👉 **My Banks**: Check the complete list of all connected banks with respective balances, account details\n\n👉 **Transaction History**: Includes pagination and filtering options for viewing transaction history of different banks\n\n👉 **Real-time Updates**: Reflects changes across all relevant pages upon connecting new bank accounts.\n\n👉 **Funds Transfer**: Allows users to transfer funds using Dwolla to other accounts with required fields and recipient bank ID.\n\n👉 **Responsiveness**: Ensures the application adapts seamlessly to various screen sizes and devices, providing a consistent user experience across desktop, tablet, and mobile platforms.\n\nand many more, including code architecture and reusability. \n\n## \u003Ca name=\"quick-start\">🤸 Quick Start\u003C\u002Fa>\n\nFollow these steps to set up the project locally on your machine.\n\n**Prerequisites**\n\nMake sure you have the following installed on your machine:\n\n- [Git](https:\u002F\u002Fgit-scm.com\u002F)\n- [Node.js](https:\u002F\u002Fnodejs.org\u002Fen)\n- [npm](https:\u002F\u002Fwww.npmjs.com\u002F) (Node Package Manager)\n\n**Cloning the Repository**\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fadrianhajdin\u002Fbanking.git\ncd banking\n```\n\n**Installation**\n\nInstall the project dependencies using npm:\n\n```bash\nnpm install\n```\n\n**Set Up Environment Variables**\n\nCreate a new file named `.env` in the root of your project and add the following content:\n\n```env\n#NEXT\nNEXT_PUBLIC_SITE_URL=\n\n#APPWRITE\nNEXT_PUBLIC_APPWRITE_ENDPOINT=https:\u002F\u002Fcloud.appwrite.io\u002Fv1\nNEXT_PUBLIC_APPWRITE_PROJECT=\nAPPWRITE_DATABASE_ID=\nAPPWRITE_USER_COLLECTION_ID=\nAPPWRITE_BANK_COLLECTION_ID=\nAPPWRITE_TRANSACTION_COLLECTION_ID=\nAPPWRITE_SECRET=\n\n#PLAID\nPLAID_CLIENT_ID=\nPLAID_SECRET=\nPLAID_ENV=\nPLAID_PRODUCTS=\nPLAID_COUNTRY_CODES=\n\n#DWOLLA\nDWOLLA_KEY=\nDWOLLA_SECRET=\nDWOLLA_BASE_URL=https:\u002F\u002Fapi-sandbox.dwolla.com\nDWOLLA_ENV=sandbox\n\n```\n\nReplace the placeholder values with your actual respective account credentials. You can obtain these credentials by signing up on the [Appwrite](https:\u002F\u002Fappwrite.io\u002F?utm_source=youtube&utm_content=reactnative&ref=JSmastery), [Plaid](https:\u002F\u002Fplaid.com\u002F) and [Dwolla](https:\u002F\u002Fwww.dwolla.com\u002F)\n\n**Running the Project**\n\n```bash\nnpm run dev\n```\n\nOpen [http:\u002F\u002Flocalhost:3000](http:\u002F\u002Flocalhost:3000) in your browser to view the project.\n\n## \u003Ca name=\"snippets\">🕸️ Snippets\u003C\u002Fa>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>.env.example\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```env\n#NEXT\nNEXT_PUBLIC_SITE_URL=\n\n#APPWRITE\nNEXT_PUBLIC_APPWRITE_ENDPOINT=https:\u002F\u002Fcloud.appwrite.io\u002Fv1\nNEXT_PUBLIC_APPWRITE_PROJECT=\nAPPWRITE_DATABASE_ID=\nAPPWRITE_USER_COLLECTION_ID=\nAPPWRITE_BANK_COLLECTION_ID=\nAPPWRITE_TRANSACTION_COLLECTION_ID=\nAPPWRITE_SECRET=\n\n#PLAID\nPLAID_CLIENT_ID=\nPLAID_SECRET=\nPLAID_ENV=sandbox\nPLAID_PRODUCTS=auth,transactions,identity\nPLAID_COUNTRY_CODES=US,CA\n\n#DWOLLA\nDWOLLA_KEY=\nDWOLLA_SECRET=\nDWOLLA_BASE_URL=https:\u002F\u002Fapi-sandbox.dwolla.com\nDWOLLA_ENV=sandbox\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>exchangePublicToken\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\u002F\u002F This function exchanges a public token for an access token and item ID\nexport const exchangePublicToken = async ({\n  publicToken,\n  user,\n}: exchangePublicTokenProps) => {\n  try {\n    \u002F\u002F Exchange public token for access token and item ID\n    const response = await plaidClient.itemPublicTokenExchange({\n      public_token: publicToken,\n    });\n\n    const accessToken = response.data.access_token;\n    const itemId = response.data.item_id;\n\n    \u002F\u002F Get account information from Plaid using the access token\n    const accountsResponse = await plaidClient.accountsGet({\n      access_token: accessToken,\n    });\n\n    const accountData = accountsResponse.data.accounts[0];\n\n    \u002F\u002F Create a processor token for Dwolla using the access token and account ID\n    const request: ProcessorTokenCreateRequest = {\n      access_token: accessToken,\n      account_id: accountData.account_id,\n      processor: \"dwolla\" as ProcessorTokenCreateRequestProcessorEnum,\n    };\n\n    const processorTokenResponse =\n      await plaidClient.processorTokenCreate(request);\n    const processorToken = processorTokenResponse.data.processor_token;\n\n    \u002F\u002F Create a funding source URL for the account using the Dwolla customer ID, processor token, and bank name\n    const fundingSourceUrl = await addFundingSource({\n      dwollaCustomerId: user.dwollaCustomerId,\n      processorToken,\n      bankName: accountData.name,\n    });\n\n    \u002F\u002F If the funding source URL is not created, throw an error\n    if (!fundingSourceUrl) throw Error;\n\n    \u002F\u002F Create a bank account using the user ID, item ID, account ID, access token, funding source URL, and sharable ID\n    await createBankAccount({\n      userId: user.$id,\n      bankId: itemId,\n      accountId: accountData.account_id,\n      accessToken,\n      fundingSourceUrl,\n      sharableId: encryptId(accountData.account_id),\n    });\n\n    \u002F\u002F Revalidate the path to reflect the changes\n    revalidatePath(\"\u002F\");\n\n    \u002F\u002F Return a success message\n    return parseStringify({\n      publicTokenExchange: \"complete\",\n    });\n  } catch (error) {\n    \u002F\u002F Log any errors that occur during the process\n    console.error(\"An error occurred while creating exchanging token:\", error);\n  }\n};\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>user.actions.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use server\";\n\nimport { revalidatePath } from \"next\u002Fcache\";\nimport { cookies } from \"next\u002Fheaders\";\nimport { ID, Query } from \"node-appwrite\";\nimport {\n  CountryCode,\n  ProcessorTokenCreateRequest,\n  ProcessorTokenCreateRequestProcessorEnum,\n  Products,\n} from \"plaid\";\n\nimport { plaidClient } from \"@\u002Flib\u002Fplaid.config\";\nimport {\n  parseStringify,\n  extractCustomerIdFromUrl,\n  encryptId,\n} from \"@\u002Flib\u002Futils\";\n\nimport { createAdminClient, createSessionClient } from \"..\u002Fappwrite.config\";\n\nimport { addFundingSource, createDwollaCustomer } from \".\u002Fdwolla.actions\";\n\nconst {\n  APPWRITE_DATABASE_ID: DATABASE_ID,\n  APPWRITE_USER_COLLECTION_ID: USER_COLLECTION_ID,\n  APPWRITE_BANK_COLLECTION_ID: BANK_COLLECTION_ID,\n} = process.env;\n\nexport const signUp = async ({ password, ...userData }: SignUpParams) => {\n  let newUserAccount;\n\n  try {\n    \u002F\u002F create appwrite user\n    const { database, account } = await createAdminClient();\n    newUserAccount = await account.create(\n      ID.unique(),\n      userData.email,\n      password,\n      `${userData.firstName} ${userData.lastName}`\n    );\n\n    if (!newUserAccount) throw new Error(\"Error creating user\");\n\n    \u002F\u002F create dwolla customer\n    const dwollaCustomerUrl = await createDwollaCustomer({\n      ...userData,\n      type: \"personal\",\n    });\n\n    if (!dwollaCustomerUrl) throw new Error(\"Error creating dwolla customer\");\n    const dwollaCustomerId = extractCustomerIdFromUrl(dwollaCustomerUrl);\n\n    const newUser = await database.createDocument(\n      DATABASE_ID!,\n      USER_COLLECTION_ID!,\n      ID.unique(),\n      {\n        ...userData,\n        userId: newUserAccount.$id,\n        dwollaCustomerUrl,\n        dwollaCustomerId,\n      }\n    );\n\n    const session = await account.createEmailPasswordSession(\n      userData.email,\n      password\n    );\n\n    cookies().set(\"appwrite-session\", session.secret, {\n      path: \"\u002F\",\n      httpOnly: true,\n      sameSite: \"strict\",\n      secure: true,\n    });\n\n    return parseStringify(newUser);\n  } catch (error) {\n    console.error(\"Error\", error);\n\n    \u002F\u002F check if account has been created, if so, delete it\n    if (newUserAccount?.$id) {\n      const { user } = await createAdminClient();\n      await user.delete(newUserAccount?.$id);\n    }\n\n    return null;\n  }\n};\n\nexport const signIn = async ({ email, password }: signInProps) => {\n  try {\n    const { account } = await createAdminClient();\n    const session = await account.createEmailPasswordSession(email, password);\n\n    cookies().set(\"appwrite-session\", session.secret, {\n      path: \"\u002F\",\n      httpOnly: true,\n      sameSite: \"strict\",\n      secure: true,\n    });\n\n    const user = await getUserInfo({ userId: session.userId });\n\n    return parseStringify(user);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\nexport const getLoggedInUser = async () => {\n  try {\n    const { account } = await createSessionClient();\n    const result = await account.get();\n\n    const user = await getUserInfo({ userId: result.$id });\n\n    return parseStringify(user);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\n\u002F\u002F CREATE PLAID LINK TOKEN\nexport const createLinkToken = async (user: User) => {\n  try {\n    const tokeParams = {\n      user: {\n        client_user_id: user.$id,\n      },\n      client_name: user.firstName + user.lastName,\n      products: [\"auth\"] as Products[],\n      language: \"en\",\n      country_codes: [\"US\"] as CountryCode[],\n    };\n\n    const response = await plaidClient.linkTokenCreate(tokeParams);\n\n    return parseStringify({ linkToken: response.data.link_token });\n  } catch (error) {\n    console.error(\n      \"An error occurred while creating a new Horizon user:\",\n      error\n    );\n  }\n};\n\n\u002F\u002F EXCHANGE PLAID PUBLIC TOKEN\n\u002F\u002F This function exchanges a public token for an access token and item ID\nexport const exchangePublicToken = async ({\n  publicToken,\n  user,\n}: exchangePublicTokenProps) => {\n  try {\n    \u002F\u002F Exchange public token for access token and item ID\n    const response = await plaidClient.itemPublicTokenExchange({\n      public_token: publicToken,\n    });\n\n    const accessToken = response.data.access_token;\n    const itemId = response.data.item_id;\n\n    \u002F\u002F Get account information from Plaid using the access token\n    const accountsResponse = await plaidClient.accountsGet({\n      access_token: accessToken,\n    });\n\n    const accountData = accountsResponse.data.accounts[0];\n\n    \u002F\u002F Create a processor token for Dwolla using the access token and account ID\n    const request: ProcessorTokenCreateRequest = {\n      access_token: accessToken,\n      account_id: accountData.account_id,\n      processor: \"dwolla\" as ProcessorTokenCreateRequestProcessorEnum,\n    };\n\n    const processorTokenResponse =\n      await plaidClient.processorTokenCreate(request);\n    const processorToken = processorTokenResponse.data.processor_token;\n\n    \u002F\u002F Create a funding source URL for the account using the Dwolla customer ID, processor token, and bank name\n    const fundingSourceUrl = await addFundingSource({\n      dwollaCustomerId: user.dwollaCustomerId,\n      processorToken,\n      bankName: accountData.name,\n    });\n\n    \u002F\u002F If the funding source URL is not created, throw an error\n    if (!fundingSourceUrl) throw Error;\n\n    \u002F\u002F Create a bank account using the user ID, item ID, account ID, access token, funding source URL, and sharable ID\n    await createBankAccount({\n      userId: user.$id,\n      bankId: itemId,\n      accountId: accountData.account_id,\n      accessToken,\n      fundingSourceUrl,\n      sharableId: encryptId(accountData.account_id),\n    });\n\n    \u002F\u002F Revalidate the path to reflect the changes\n    revalidatePath(\"\u002F\");\n\n    \u002F\u002F Return a success message\n    return parseStringify({\n      publicTokenExchange: \"complete\",\n    });\n  } catch (error) {\n    \u002F\u002F Log any errors that occur during the process\n    console.error(\"An error occurred while creating exchanging token:\", error);\n  }\n};\n\nexport const getUserInfo = async ({ userId }: getUserInfoProps) => {\n  try {\n    const { database } = await createAdminClient();\n\n    const user = await database.listDocuments(\n      DATABASE_ID!,\n      USER_COLLECTION_ID!,\n      [Query.equal(\"userId\", [userId])]\n    );\n\n    if (user.total !== 1) return null;\n\n    return parseStringify(user.documents[0]);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\nexport const createBankAccount = async ({\n  accessToken,\n  userId,\n  accountId,\n  bankId,\n  fundingSourceUrl,\n  sharableId,\n}: createBankAccountProps) => {\n  try {\n    const { database } = await createAdminClient();\n\n    const bankAccount = await database.createDocument(\n      DATABASE_ID!,\n      BANK_COLLECTION_ID!,\n      ID.unique(),\n      {\n        accessToken,\n        userId,\n        accountId,\n        bankId,\n        fundingSourceUrl,\n        sharableId,\n      }\n    );\n\n    return parseStringify(bankAccount);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\n\u002F\u002F get user bank accounts\nexport const getBanks = async ({ userId }: getBanksProps) => {\n  try {\n    const { database } = await createAdminClient();\n\n    const banks = await database.listDocuments(\n      DATABASE_ID!,\n      BANK_COLLECTION_ID!,\n      [Query.equal(\"userId\", [userId])]\n    );\n\n    return parseStringify(banks.documents);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\n\u002F\u002F get specific bank from bank collection by document id\nexport const getBank = async ({ documentId }: getBankProps) => {\n  try {\n    const { database } = await createAdminClient();\n\n    const bank = await database.listDocuments(\n      DATABASE_ID!,\n      BANK_COLLECTION_ID!,\n      [Query.equal(\"$id\", [documentId])]\n    );\n\n    if (bank.total !== 1) return null;\n\n    return parseStringify(bank.documents[0]);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n\n\u002F\u002F get specific bank from bank collection by account id\nexport const getBankByAccountId = async ({\n  accountId,\n}: getBankByAccountIdProps) => {\n  try {\n    const { database } = await createAdminClient();\n\n    const bank = await database.listDocuments(\n      DATABASE_ID!,\n      BANK_COLLECTION_ID!,\n      [Query.equal(\"accountId\", [accountId])]\n    );\n\n    if (bank.total !== 1) return null;\n\n    return parseStringify(bank.documents[0]);\n  } catch (error) {\n    console.error(\"Error\", error);\n    return null;\n  }\n};\n```\n  \n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>dwolla.actions.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use server\";\n\nimport { Client } from \"dwolla-v2\";\n\nconst getEnvironment = (): \"production\" | \"sandbox\" => {\n  const environment = process.env.DWOLLA_ENV as string;\n\n  switch (environment) {\n    case \"sandbox\":\n      return \"sandbox\";\n    case \"production\":\n      return \"production\";\n    default:\n      throw new Error(\n        \"Dwolla environment should either be set to `sandbox` or `production`\"\n      );\n  }\n};\n\nconst dwollaClient = new Client({\n  environment: getEnvironment(),\n  key: process.env.DWOLLA_KEY as string,\n  secret: process.env.DWOLLA_SECRET as string,\n});\n\n\u002F\u002F Create a Dwolla Funding Source using a Plaid Processor Token\nexport const createFundingSource = async (\n  options: CreateFundingSourceOptions\n) => {\n  try {\n    return await dwollaClient\n      .post(`customers\u002F${options.customerId}\u002Ffunding-sources`, {\n        name: options.fundingSourceName,\n        plaidToken: options.plaidToken,\n      })\n      .then((res) => res.headers.get(\"location\"));\n  } catch (err) {\n    console.error(\"Creating a Funding Source Failed: \", err);\n  }\n};\n\nexport const createOnDemandAuthorization = async () => {\n  try {\n    const onDemandAuthorization = await dwollaClient.post(\n      \"on-demand-authorizations\"\n    );\n    const authLink = onDemandAuthorization.body._links;\n    return authLink;\n  } catch (err) {\n    console.error(\"Creating an On Demand Authorization Failed: \", err);\n  }\n};\n\nexport const createDwollaCustomer = async (\n  newCustomer: NewDwollaCustomerParams\n) => {\n  try {\n    return await dwollaClient\n      .post(\"customers\", newCustomer)\n      .then((res) => res.headers.get(\"location\"));\n  } catch (err) {\n    console.error(\"Creating a Dwolla Customer Failed: \", err);\n  }\n};\n\nexport const createTransfer = async ({\n  sourceFundingSourceUrl,\n  destinationFundingSourceUrl,\n  amount,\n}: TransferParams) => {\n  try {\n    const requestBody = {\n      _links: {\n        source: {\n          href: sourceFundingSourceUrl,\n        },\n        destination: {\n          href: destinationFundingSourceUrl,\n        },\n      },\n      amount: {\n        currency: \"USD\",\n        value: amount,\n      },\n    };\n    return await dwollaClient\n      .post(\"transfers\", requestBody)\n      .then((res) => res.headers.get(\"location\"));\n  } catch (err) {\n    console.error(\"Transfer fund failed: \", err);\n  }\n};\n\nexport const addFundingSource = async ({\n  dwollaCustomerId,\n  processorToken,\n  bankName,\n}: AddFundingSourceParams) => {\n  try {\n    \u002F\u002F create dwolla auth link\n    const dwollaAuthLinks = await createOnDemandAuthorization();\n\n    \u002F\u002F add funding source to the dwolla customer & get the funding source url\n    const fundingSourceOptions = {\n      customerId: dwollaCustomerId,\n      fundingSourceName: bankName,\n      plaidToken: processorToken,\n      _links: dwollaAuthLinks,\n    };\n    return await createFundingSource(fundingSourceOptions);\n  } catch (err) {\n    console.error(\"Transfer fund failed: \", err);\n  }\n};\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>bank.actions.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use server\";\n\nimport {\n  ACHClass,\n  CountryCode,\n  TransferAuthorizationCreateRequest,\n  TransferCreateRequest,\n  TransferNetwork,\n  TransferType,\n} from \"plaid\";\n\nimport { plaidClient } from \"..\u002Fplaid.config\";\nimport { parseStringify } from \"..\u002Futils\";\n\nimport { getTransactionsByBankId } from \".\u002Ftransaction.actions\";\nimport { getBanks, getBank } from \".\u002Fuser.actions\";\n\n\u002F\u002F Get multiple bank accounts\nexport const getAccounts = async ({ userId }: getAccountsProps) => {\n  try {\n    \u002F\u002F get banks from db\n    const banks = await getBanks({ userId });\n\n    const accounts = await Promise.all(\n      banks?.map(async (bank: Bank) => {\n        \u002F\u002F get each account info from plaid\n        const accountsResponse = await plaidClient.accountsGet({\n          access_token: bank.accessToken,\n        });\n        const accountData = accountsResponse.data.accounts[0];\n\n        \u002F\u002F get institution info from plaid\n        const institution = await getInstitution({\n          institutionId: accountsResponse.data.item.institution_id!,\n        });\n\n        const account = {\n          id: accountData.account_id,\n          availableBalance: accountData.balances.available!,\n          currentBalance: accountData.balances.current!,\n          institutionId: institution.institution_id,\n          name: accountData.name,\n          officialName: accountData.official_name,\n          mask: accountData.mask!,\n          type: accountData.type as string,\n          subtype: accountData.subtype! as string,\n          appwriteItemId: bank.$id,\n          sharableId: bank.sharableId,\n        };\n\n        return account;\n      })\n    );\n\n    const totalBanks = accounts.length;\n    const totalCurrentBalance = accounts.reduce((total, account) => {\n      return total + account.currentBalance;\n    }, 0);\n\n    return parseStringify({ data: accounts, totalBanks, totalCurrentBalance });\n  } catch (error) {\n    console.error(\"An error occurred while getting the accounts:\", error);\n  }\n};\n\n\u002F\u002F Get one bank account\nexport const getAccount = async ({ appwriteItemId }: getAccountProps) => {\n  try {\n    \u002F\u002F get bank from db\n    const bank = await getBank({ documentId: appwriteItemId });\n\n    \u002F\u002F get account info from plaid\n    const accountsResponse = await plaidClient.accountsGet({\n      access_token: bank.accessToken,\n    });\n    const accountData = accountsResponse.data.accounts[0];\n\n    \u002F\u002F get transfer transactions from appwrite\n    const transferTransactionsData = await getTransactionsByBankId({\n      bankId: bank.$id,\n    });\n\n    const transferTransactions = transferTransactionsData.documents.map(\n      (transferData: Transaction) => ({\n        id: transferData.$id,\n        name: transferData.name!,\n        amount: transferData.amount!,\n        date: transferData.$createdAt,\n        paymentChannel: transferData.channel,\n        category: transferData.category,\n        type: transferData.senderBankId === bank.$id ? \"debit\" : \"credit\",\n      })\n    );\n\n    \u002F\u002F get institution info from plaid\n    const institution = await getInstitution({\n      institutionId: accountsResponse.data.item.institution_id!,\n    });\n\n    const transactions = await getTransactions({\n      accessToken: bank?.accessToken,\n    });\n\n    const account = {\n      id: accountData.account_id,\n      availableBalance: accountData.balances.available!,\n      currentBalance: accountData.balances.current!,\n      institutionId: institution.institution_id,\n      name: accountData.name,\n      officialName: accountData.official_name,\n      mask: accountData.mask!,\n      type: accountData.type as string,\n      subtype: accountData.subtype! as string,\n      appwriteItemId: bank.$id,\n    };\n\n    \u002F\u002F sort transactions by date such that the most recent transaction is first\n    const allTransactions = [...transactions, ...transferTransactions].sort(\n      (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()\n    );\n\n    return parseStringify({\n      data: account,\n      transactions: allTransactions,\n    });\n  } catch (error) {\n    console.error(\"An error occurred while getting the account:\", error);\n  }\n};\n\n\u002F\u002F Get bank info\nexport const getInstitution = async ({\n  institutionId,\n}: getInstitutionProps) => {\n  try {\n    const institutionResponse = await plaidClient.institutionsGetById({\n      institution_id: institutionId,\n      country_codes: [\"US\"] as CountryCode[],\n    });\n\n    const intitution = institutionResponse.data.institution;\n\n    return parseStringify(intitution);\n  } catch (error) {\n    console.error(\"An error occurred while getting the accounts:\", error);\n  }\n};\n\n\u002F\u002F Get transactions\nexport const getTransactions = async ({\n  accessToken,\n}: getTransactionsProps) => {\n  let hasMore = true;\n  let transactions: any = [];\n\n  try {\n    \u002F\u002F Iterate through each page of new transaction updates for item\n    while (hasMore) {\n      const response = await plaidClient.transactionsSync({\n        access_token: accessToken,\n      });\n\n      const data = response.data;\n\n      transactions = response.data.added.map((transaction) => ({\n        id: transaction.transaction_id,\n        name: transaction.name,\n        paymentChannel: transaction.payment_channel,\n        type: transaction.payment_channel,\n        accountId: transaction.account_id,\n        amount: transaction.amount,\n        pending: transaction.pending,\n        category: transaction.category ? transaction.category[0] : \"\",\n        date: transaction.date,\n        image: transaction.logo_url,\n      }));\n\n      hasMore = data.has_more;\n    }\n\n    return parseStringify(transactions);\n  } catch (error) {\n    console.error(\"An error occurred while getting the accounts:\", error);\n  }\n};\n\n\u002F\u002F Create Transfer\nexport const createTransfer = async () => {\n  const transferAuthRequest: TransferAuthorizationCreateRequest = {\n    access_token: \"access-sandbox-cddd20c1-5ba8-4193-89f9-3a0b91034c25\",\n    account_id: \"Zl8GWV1jqdTgjoKnxQn1HBxxVBanm5FxZpnQk\",\n    funding_account_id: \"442d857f-fe69-4de2-a550-0c19dc4af467\",\n    type: \"credit\" as TransferType,\n    network: \"ach\" as TransferNetwork,\n    amount: \"10.00\",\n    ach_class: \"ppd\" as ACHClass,\n    user: {\n      legal_name: \"Anne Charleston\",\n    },\n  };\n  try {\n    const transferAuthResponse =\n      await plaidClient.transferAuthorizationCreate(transferAuthRequest);\n    const authorizationId = transferAuthResponse.data.authorization.id;\n\n    const transferCreateRequest: TransferCreateRequest = {\n      access_token: \"access-sandbox-cddd20c1-5ba8-4193-89f9-3a0b91034c25\",\n      account_id: \"Zl8GWV1jqdTgjoKnxQn1HBxxVBanm5FxZpnQk\",\n      description: \"payment\",\n      authorization_id: authorizationId,\n    };\n\n    const responseCreateResponse = await plaidClient.transferCreate(\n      transferCreateRequest\n    );\n\n    const transfer = responseCreateResponse.data.transfer;\n    return parseStringify(transfer);\n  } catch (error) {\n    console.error(\n      \"An error occurred while creating transfer authorization:\",\n      error\n    );\n  }\n};\n```\n\n\u003C\u002Fdetails>\n\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>BankTabItem.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\n\nimport { useSearchParams, useRouter } from \"next\u002Fnavigation\";\n\nimport { cn, formUrlQuery } from \"@\u002Flib\u002Futils\";\n\nexport const BankTabItem = ({ account, appwriteItemId }: BankTabItemProps) => {\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const isActive = appwriteItemId === account?.appwriteItemId;\n\n  const handleBankChange = () => {\n    const newUrl = formUrlQuery({\n      params: searchParams.toString(),\n      key: \"id\",\n      value: account?.appwriteItemId,\n    });\n    router.push(newUrl, { scroll: false });\n  };\n\n  return (\n    \u003Cdiv\n      onClick={handleBankChange}\n      className={cn(`banktab-item`, {\n        \" border-blue-600\": isActive,\n      })}\n    >\n      \u003Cp\n        className={cn(`text-16 line-clamp-1 flex-1 font-medium text-gray-500`, {\n          \" text-blue-600\": isActive,\n        })}\n      >\n        {account.name}\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n};\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>BankInfo.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\n\nimport Image from \"next\u002Fimage\";\nimport { useSearchParams, useRouter } from \"next\u002Fnavigation\";\n\nimport {\n  cn,\n  formUrlQuery,\n  formatAmount,\n  getAccountTypeColors,\n} from \"@\u002Flib\u002Futils\";\n\nconst BankInfo = ({ account, appwriteItemId, type }: BankInfoProps) => {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n\n  const isActive = appwriteItemId === account?.appwriteItemId;\n\n  const handleBankChange = () => {\n    const newUrl = formUrlQuery({\n      params: searchParams.toString(),\n      key: \"id\",\n      value: account?.appwriteItemId,\n    });\n    router.push(newUrl, { scroll: false });\n  };\n\n  const colors = getAccountTypeColors(account?.type as AccountTypes);\n\n  return (\n    \u003Cdiv\n      onClick={handleBankChange}\n      className={cn(`bank-info ${colors.bg}`, {\n        \"shadow-sm border-blue-700\": type === \"card\" && isActive,\n        \"rounded-xl\": type === \"card\",\n        \"hover:shadow-sm cursor-pointer\": type === \"card\",\n      })}\n    >\n      \u003Cfigure\n        className={`flex-center h-fit rounded-full bg-blue-100 ${colors.lightBg}`}\n      >\n        \u003CImage\n          src=\"\u002Ficons\u002Fconnect-bank.svg\"\n          width={20}\n          height={20}\n          alt={account.subtype}\n          className=\"m-2 min-w-5\"\n        \u002F>\n      \u003C\u002Ffigure>\n      \u003Cdiv className=\"flex w-full flex-1 flex-col justify-center gap-1\">\n        \u003Cdiv className=\"bank-info_content\">\n          \u003Ch2\n            className={`text-16 line-clamp-1 flex-1 font-bold text-blue-900 ${colors.title}`}\n          >\n            {account.name}\n          \u003C\u002Fh2>\n          {type === \"full\" && (\n            \u003Cp\n              className={`text-12 rounded-full px-3 py-1 font-medium text-blue-700 ${colors.subText} ${colors.lightBg}`}\n            >\n              {account.subtype}\n            \u003C\u002Fp>\n          )}\n        \u003C\u002Fdiv>\n\n        \u003Cp className={`text-16 font-medium text-blue-700 ${colors.subText}`}>\n          {formatAmount(account.currentBalance)}\n        \u003C\u002Fp>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n};\n\nexport default BankInfo;\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>Copy.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\nimport { useState } from \"react\";\n\nimport { Button } from \".\u002Fui\u002Fbutton\";\n\nconst Copy = ({ title }: { title: string }) => {\n  const [hasCopied, setHasCopied] = useState(false);\n\n  const copyToClipboard = () => {\n    navigator.clipboard.writeText(title);\n    setHasCopied(true);\n\n    setTimeout(() => {\n      setHasCopied(false);\n    }, 2000);\n  };\n\n  return (\n    \u003CButton\n      data-state=\"closed\"\n      className=\"mt-3 flex max-w-[320px] gap-4\"\n      variant=\"secondary\"\n      onClick={copyToClipboard}\n    >\n      \u003Cp className=\"line-clamp-1 w-full max-w-full text-xs font-medium text-black-2\">\n        {title}\n      \u003C\u002Fp>\n\n      {!hasCopied ? (\n        \u003Csvg\n          xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          className=\"mr-2 size-4\"\n        >\n          \u003Crect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\">\u003C\u002Frect>\n          \u003Cpath d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\">\u003C\u002Fpath>\n        \u003C\u002Fsvg>\n      ) : (\n        \u003Csvg\n          xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          className=\"mr-2 size-4\"\n        >\n          \u003Cpolyline points=\"20 6 9 17 4 12\">\u003C\u002Fpolyline>\n        \u003C\u002Fsvg>\n      )}\n    \u003C\u002FButton>\n  );\n};\n\nexport default Copy;\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>PaymentTransferForm.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\n\nimport { zodResolver } from \"@hookform\u002Fresolvers\u002Fzod\";\nimport { Loader2 } from \"lucide-react\";\nimport { useRouter } from \"next\u002Fnavigation\";\nimport { useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport * as z from \"zod\";\n\nimport { createTransfer } from \"@\u002Flib\u002Factions\u002Fdwolla.actions\";\nimport { createTransaction } from \"@\u002Flib\u002Factions\u002Ftransaction.actions\";\nimport { getBank, getBankByAccountId } from \"@\u002Flib\u002Factions\u002Fuser.actions\";\nimport { decryptId } from \"@\u002Flib\u002Futils\";\n\nimport { BankDropdown } from \".\u002Fbank\u002FBankDropdown\";\nimport { Button } from \".\u002Fui\u002Fbutton\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \".\u002Fui\u002Fform\";\nimport { Input } from \".\u002Fui\u002Finput\";\nimport { Textarea } from \".\u002Fui\u002Ftextarea\";\n\nconst formSchema = z.object({\n  email: z.string().email(\"Invalid email address\"),\n  name: z.string().min(4, \"Transfer note is too short\"),\n  amount: z.string().min(4, \"Amount is too short\"),\n  senderBank: z.string().min(4, \"Please select a valid bank account\"),\n  sharableId: z.string().min(8, \"Please select a valid sharable Id\"),\n});\n\nconst PaymentTransferForm = ({ accounts }: PaymentTransferFormProps) => {\n  const router = useRouter();\n  const [isLoading, setIsLoading] = useState(false);\n\n  const form = useForm\u003Cz.infer\u003Ctypeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      name: \"\",\n      email: \"\",\n      amount: \"\",\n      senderBank: \"\",\n      sharableId: \"\",\n    },\n  });\n\n  const submit = async (data: z.infer\u003Ctypeof formSchema>) => {\n    setIsLoading(true);\n\n    try {\n      const receiverAccountId = decryptId(data.sharableId);\n      const receiverBank = await getBankByAccountId({\n        accountId: receiverAccountId,\n      });\n      const senderBank = await getBank({ documentId: data.senderBank });\n\n      const transferParams = {\n        sourceFundingSourceUrl: senderBank.fundingSourceUrl,\n        destinationFundingSourceUrl: receiverBank.fundingSourceUrl,\n        amount: data.amount,\n      };\n      \u002F\u002F create transfer\n      const transfer = await createTransfer(transferParams);\n\n      \u002F\u002F create transfer transaction\n      if (transfer) {\n        const transaction = {\n          name: data.name,\n          amount: data.amount,\n          senderId: senderBank.userId.$id,\n          senderBankId: senderBank.$id,\n          receiverId: receiverBank.userId.$id,\n          receiverBankId: receiverBank.$id,\n          email: data.email,\n        };\n\n        const newTransaction = await createTransaction(transaction);\n\n        if (newTransaction) {\n          form.reset();\n          router.push(\"\u002F\");\n        }\n      }\n    } catch (error) {\n      console.error(\"Submitting create transfer request failed: \", error);\n    }\n\n    setIsLoading(false);\n  };\n\n  return (\n    \u003CForm {...form}>\n      \u003Cform onSubmit={form.handleSubmit(submit)} className=\"flex flex-col\">\n        \u003CFormField\n          control={form.control}\n          name=\"senderBank\"\n          render={() => (\n            \u003CFormItem className=\"border-t border-gray-200\">\n              \u003Cdiv className=\"payment-transfer_form-item pb-6 pt-5\">\n                \u003Cdiv className=\"payment-transfer_form-content\">\n                  \u003CFormLabel className=\"text-14 font-medium text-gray-700\">\n                    Select Source Bank\n                  \u003C\u002FFormLabel>\n                  \u003CFormDescription className=\"text-12 font-normal text-gray-600\">\n                    Select the bank account you want to transfer funds from\n                  \u003C\u002FFormDescription>\n                \u003C\u002Fdiv>\n                \u003Cdiv className=\"flex w-full flex-col\">\n                  \u003CFormControl>\n                    \u003CBankDropdown\n                      accounts={accounts}\n                      setValue={form.setValue}\n                      otherStyles=\"!w-full\"\n                    \u002F>\n                  \u003C\u002FFormControl>\n                  \u003CFormMessage className=\"text-12 text-red-500\" \u002F>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002FFormItem>\n          )}\n        \u002F>\n\n        \u003CFormField\n          control={form.control}\n          name=\"name\"\n          render={({ field }) => (\n            \u003CFormItem className=\"border-t border-gray-200\">\n              \u003Cdiv className=\"payment-transfer_form-item pb-6 pt-5\">\n                \u003Cdiv className=\"payment-transfer_form-content\">\n                  \u003CFormLabel className=\"text-14 font-medium text-gray-700\">\n                    Transfer Note (Optional)\n                  \u003C\u002FFormLabel>\n                  \u003CFormDescription className=\"text-12 font-normal text-gray-600\">\n                    Please provide any additional information or instructions\n                    related to the transfer\n                  \u003C\u002FFormDescription>\n                \u003C\u002Fdiv>\n                \u003Cdiv className=\"flex w-full flex-col\">\n                  \u003CFormControl>\n                    \u003CTextarea\n                      placeholder=\"Write a short note here\"\n                      className=\"input-class\"\n                      {...field}\n                    \u002F>\n                  \u003C\u002FFormControl>\n                  \u003CFormMessage className=\"text-12 text-red-500\" \u002F>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002FFormItem>\n          )}\n        \u002F>\n\n        \u003Cdiv className=\"payment-transfer_form-details\">\n          \u003Ch2 className=\"text-18 font-semibold text-gray-900\">\n            Bank account details\n          \u003C\u002Fh2>\n          \u003Cp className=\"text-16 font-normal text-gray-600\">\n            Enter the bank account details of the recipient\n          \u003C\u002Fp>\n        \u003C\u002Fdiv>\n\n        \u003CFormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) => (\n            \u003CFormItem className=\"border-t border-gray-200\">\n              \u003Cdiv className=\"payment-transfer_form-item py-5\">\n                \u003CFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\">\n                  Recipient&apos;s Email Address\n                \u003C\u002FFormLabel>\n                \u003Cdiv className=\"flex w-full flex-col\">\n                  \u003CFormControl>\n                    \u003CInput\n                      placeholder=\"ex: johndoe@gmail.com\"\n                      className=\"input-class\"\n                      {...field}\n                    \u002F>\n                  \u003C\u002FFormControl>\n                  \u003CFormMessage className=\"text-12 text-red-500\" \u002F>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002FFormItem>\n          )}\n        \u002F>\n\n        \u003CFormField\n          control={form.control}\n          name=\"sharableId\"\n          render={({ field }) => (\n            \u003CFormItem className=\"border-t border-gray-200\">\n              \u003Cdiv className=\"payment-transfer_form-item pb-5 pt-6\">\n                \u003CFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\">\n                  Receiver&apos;s Plaid Sharable Id\n                \u003C\u002FFormLabel>\n                \u003Cdiv className=\"flex w-full flex-col\">\n                  \u003CFormControl>\n                    \u003CInput\n                      placeholder=\"Enter the public account number\"\n                      className=\"input-class\"\n                      {...field}\n                    \u002F>\n                  \u003C\u002FFormControl>\n                  \u003CFormMessage className=\"text-12 text-red-500\" \u002F>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002FFormItem>\n          )}\n        \u002F>\n\n        \u003CFormField\n          control={form.control}\n          name=\"amount\"\n          render={({ field }) => (\n            \u003CFormItem className=\"border-y border-gray-200\">\n              \u003Cdiv className=\"payment-transfer_form-item py-5\">\n                \u003CFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\">\n                  Amount\n                \u003C\u002FFormLabel>\n                \u003Cdiv className=\"flex w-full flex-col\">\n                  \u003CFormControl>\n                    \u003CInput\n                      placeholder=\"ex: 5.00\"\n                      className=\"input-class\"\n                      {...field}\n                    \u002F>\n                  \u003C\u002FFormControl>\n                  \u003CFormMessage className=\"text-12 text-red-500\" \u002F>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002FFormItem>\n          )}\n        \u002F>\n\n        \u003Cdiv className=\"payment-transfer_btn-box\">\n          \u003CButton type=\"submit\" className=\"payment-transfer_btn\">\n            {isLoading ? (\n              \u003C>\n                \u003CLoader2 size={20} className=\"animate-spin\" \u002F> &nbsp; Sending...\n              \u003C\u002F>\n            ) : (\n              \"Transfer Funds\"\n            )}\n          \u003C\u002FButton>\n        \u003C\u002Fdiv>\n      \u003C\u002Fform>\n    \u003C\u002FForm>\n  );\n};\n\nexport default PaymentTransferForm;\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>Missing from the video (top right on the transaction list page) BankDropdown.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\n\nimport Image from \"next\u002Fimage\";\nimport { useSearchParams, useRouter } from \"next\u002Fnavigation\";\nimport { useState } from \"react\";\n\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectTrigger,\n} from \"@\u002Fcomponents\u002Fui\u002Fselect\";\nimport { formUrlQuery, formatAmount } from \"@\u002Flib\u002Futils\";\n\nexport const BankDropdown = ({\n  accounts = [],\n  setValue,\n  otherStyles,\n}: BankDropdownProps) => {\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const [selected, setSeclected] = useState(accounts[0]);\n\n  const handleBankChange = (id: string) => {\n    const account = accounts.find((account) => account.appwriteItemId === id)!;\n\n    setSeclected(account);\n    const newUrl = formUrlQuery({\n      params: searchParams.toString(),\n      key: \"id\",\n      value: id,\n    });\n    router.push(newUrl, { scroll: false });\n\n    if (setValue) {\n      setValue(\"senderBank\", id);\n    }\n  };\n\n  return (\n    \u003CSelect\n      defaultValue={selected.id}\n      onValueChange={(value) => handleBankChange(value)}\n    >\n      \u003CSelectTrigger\n        className={`flex w-full gap-3 md:w-[300px] ${otherStyles}`}\n      >\n        \u003CImage\n          src=\"icons\u002Fcredit-card.svg\"\n          width={20}\n          height={20}\n          alt=\"account\"\n        \u002F>\n        \u003Cp className=\"line-clamp-1 w-full text-left\">{selected.name}\u003C\u002Fp>\n      \u003C\u002FSelectTrigger>\n      \u003CSelectContent\n        className={`w-full md:w-[300px] ${otherStyles}`}\n        align=\"end\"\n      >\n        \u003CSelectGroup>\n          \u003CSelectLabel className=\"py-2 font-normal text-gray-500\">\n            Select a bank to display\n          \u003C\u002FSelectLabel>\n          {accounts.map((account: Account) => (\n            \u003CSelectItem\n              key={account.id}\n              value={account.appwriteItemId}\n              className=\"cursor-pointer border-t\"\n            >\n              \u003Cdiv className=\"flex flex-col \">\n                \u003Cp className=\"text-16 font-medium\">{account.name}\u003C\u002Fp>\n                \u003Cp className=\"text-14 font-medium text-blue-600\">\n                  {formatAmount(account.currentBalance)}\n                \u003C\u002Fp>\n              \u003C\u002Fdiv>\n            \u003C\u002FSelectItem>\n          ))}\n        \u003C\u002FSelectGroup>\n      \u003C\u002FSelectContent>\n    \u003C\u002FSelect>\n  );\n};\n```\n  \n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>Pagination.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\"use client\";\n\nimport Image from \"next\u002Fimage\";\nimport { useRouter, useSearchParams } from \"next\u002Fnavigation\";\n\nimport { Button } from \"@\u002Fcomponents\u002Fui\u002Fbutton\";\nimport { formUrlQuery } from \"@\u002Flib\u002Futils\";\n\nexport const Pagination = ({ page, totalPages }: PaginationProps) => {\n  const router = useRouter();\n  const searchParams = useSearchParams()!;\n\n  const handleNavigation = (type: \"prev\" | \"next\") => {\n    const pageNumber = type === \"prev\" ? page - 1 : page + 1;\n\n    const newUrl = formUrlQuery({\n      params: searchParams.toString(),\n      key: \"page\",\n      value: pageNumber.toString(),\n    });\n\n    router.push(newUrl, { scroll: false });\n  };\n\n  return (\n    \u003Cdiv className=\"flex justify-between gap-3\">\n      \u003CButton\n        size=\"lg\"\n        variant=\"ghost\"\n        className=\"p-0 hover:bg-transparent\"\n        onClick={() => handleNavigation(\"prev\")}\n        disabled={Number(page) \u003C= 1}\n      >\n        \u003CImage\n          src=\"\u002Ficons\u002Farrow-left.svg\"\n          alt=\"arrow\"\n          width={20}\n          height={20}\n          className=\"mr-2\"\n        \u002F>\n        Prev\n      \u003C\u002FButton>\n      \u003Cp className=\"text-14 flex items-center px-2\">\n        {page} \u002F {totalPages}\n      \u003C\u002Fp>\n      \u003CButton\n        size=\"lg\"\n        variant=\"ghost\"\n        className=\"p-0 hover:bg-transparent\"\n        onClick={() => handleNavigation(\"next\")}\n        disabled={Number(page) >= totalPages}\n      >\n        Next\n        \u003CImage\n          src=\"\u002Ficons\u002Farrow-left.svg\"\n          alt=\"arrow\"\n          width={20}\n          height={20}\n          className=\"ml-2 -scale-x-100\"\n        \u002F>\n      \u003C\u002FButton>\n    \u003C\u002Fdiv>\n  );\n};\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>Category.tsx\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nimport Image from \"next\u002Fimage\";\n\nimport { topCategoryStyles } from \"@\u002Fconstants\";\nimport { cn } from \"@\u002Flib\u002Futils\";\n\nimport { Progress } from \".\u002Fui\u002Fprogress\";\n\nexport const Category = ({ category }: CategoryProps) => {\n  const {\n    bg,\n    circleBg,\n    text: { main, count },\n    progress: { bg: progressBg, indicator },\n    icon,\n  } = topCategoryStyles[category.name as keyof typeof topCategoryStyles] ||\n  topCategoryStyles.default;\n\n  return (\n    \u003Cdiv className={cn(\"gap-[18px] flex p-4 rounded-xl\", bg)}>\n      \u003Cfigure className={cn(\"flex-center size-10 rounded-full\", circleBg)}>\n        \u003CImage src={icon} width={20} height={20} alt={category.name} \u002F>\n      \u003C\u002Ffigure>\n      \u003Cdiv className=\"flex w-full flex-1 flex-col gap-2\">\n        \u003Cdiv className=\"text-14 flex justify-between\">\n          \u003Ch2 className={cn(\"font-medium\", main)}>{category.name}\u003C\u002Fh2>\n          \u003Ch3 className={cn(\"font-normal\", count)}>{category.count}\u003C\u002Fh3>\n        \u003C\u002Fdiv>\n        \u003CProgress\n          value={(category.count \u002F category.totalCount) * 100}\n          className={cn(\"h-2 w-full\", progressBg)}\n          indicatorClassName={cn(\"h-2 w-full\", indicator)}\n        \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n};\n```\n\n\u003C\u002Fdetails>\n\n## \u003Ca name=\"links\">🔗 Links\u003C\u002Fa>\n\nAssets used in the project can be found [here](https:\u002F\u002Fdrive.google.com\u002Ffile\u002Fd\u002F1TVhdnD97LajGsyaiNa6sDs-ap-z1oerA\u002Fview?usp=sharing)\n\n## \u003Ca name=\"more\">🚀 More\u003C\u002Fa>\n\n**Advance your skills with Next.js Pro Course**\n\nEnjoyed creating this project? Dive deeper into our PRO courses for a richer learning experience. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!\n\n\u003Ca href=\"https:\u002F\u002Fwww.jsmastery.pro\u002Fultimate-next-course\" target=\"_blank\">\n\u003Cimg src=\"https:\u002F\u002Fi.ibb.co\u002F804sPK6\u002FImage-720.png\" alt=\"Project Banner\">\n\u003C\u002Fa>\n","Horizon 是一个面向大众的现代银行平台。它使用 Next.js 和 TypeScript 构建，具备实时显示交易、跨用户转账以及综合财务管理等功能。项目利用了 Appwrite 作为后端服务，Plaid 用于多银行账户连接，Dwolla 处理支付，并通过 React Hook Form 和 Zod 进行表单处理和数据验证。此外，TailwindCSS 和 ShadCN 保证了良好的用户体验设计，而 Chart.js 则提供了直观的数据可视化功能。此平台非常适合需要集成多种金融服务的应用场景，尤其是那些希望提供安全认证、多银行账户管理和便捷支付解决方案的金融科技产品。","2026-06-11 03:47:15","high_star"]