{"id":15136659,"url":"https://github.com/yann1294/banking","last_synced_at":"2026-01-20T20:12:02.739Z","repository":{"id":255575140,"uuid":"852150356","full_name":"yann1294/banking","owner":"yann1294","description":"Horizon is a modern banking platform for everyone","archived":false,"fork":false,"pushed_at":"2024-09-30T10:55:15.000Z","size":1105,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-23T11:40:56.225Z","etag":null,"topics":["appwrite","nextjs14","plaid-link","sassoftware","sentry-integration","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yann1294.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-04T10:05:10.000Z","updated_at":"2024-09-30T10:55:18.000Z","dependencies_parsed_at":"2025-02-08T14:41:45.885Z","dependency_job_id":null,"html_url":"https://github.com/yann1294/banking","commit_stats":null,"previous_names":["yann1294/banking"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yann1294/banking","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yann1294%2Fbanking","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yann1294%2Fbanking/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yann1294%2Fbanking/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yann1294%2Fbanking/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yann1294","download_url":"https://codeload.github.com/yann1294/banking/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yann1294%2Fbanking/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28612165,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T18:56:40.769Z","status":"ssl_error","status_checked_at":"2026-01-20T18:54:26.653Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appwrite","nextjs14","plaid-link","sassoftware","sentry-integration","typescript"],"created_at":"2024-09-26T06:40:26.511Z","updated_at":"2026-01-20T20:12:02.722Z","avatar_url":"https://github.com/yann1294.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cbr /\u003e\n    \u003ca href=\"https://www.youtube.com/watch?v=FkowOdMjvYo\" target=\"_blank\"\u003e\n      \u003cimg src=\"https://github.com/adrianhajdin/banking/assets/151519281/43071c0b-c41f-4058-840a-d03f746fa272\" alt=\"Project Banner\"\u003e\n    \u003c/a\u003e\n  \u003cbr /\u003e\n\n  \u003cdiv\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Next_JS-black?style=for-the-badge\u0026logoColor=white\u0026logo=nextdotjs\u0026color=000000\" alt=\"nextdotjs\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-TypeScript-black?style=for-the-badge\u0026logoColor=white\u0026logo=typescript\u0026color=3178C6\" alt=\"typescript\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge\u0026logoColor=white\u0026logo=tailwindcss\u0026color=06B6D4\" alt=\"tailwindcss\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Appwrite-black?style=for-the-badge\u0026logoColor=white\u0026logo=appwrite\u0026color=FD366E\" alt=\"appwrite\" /\u003e\n  \u003c/div\u003e\n\n  \u003ch3 align=\"center\"\u003eA Fintech Bank Application\u003c/h3\u003e\n\n   \u003cdiv align=\"center\"\u003e\n     Build this project step by step with our detailed tutorial on \u003ca href=\"https://www.youtube.com/@javascriptmastery/videos\" target=\"_blank\"\u003e\u003cb\u003eJavaScript Mastery\u003c/b\u003e\u003c/a\u003e YouTube. Join the JSM family!\n    \u003c/div\u003e\n\u003c/div\u003e\n\n## 📋 \u003ca name=\"table\"\u003eTable of Contents\u003c/a\u003e\n\n1. 🤖 [Introduction](#introduction)\n2. ⚙️ [Tech Stack](#tech-stack)\n3. 🔋 [Features](#features)\n4. 🤸 [Quick Start](#quick-start)\n5. 🕸️ [Snippets](#snippets)\n6. 🔗 [Links](#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://www.youtube.com/@javascriptmastery/videos\" target=\"_blank\"\u003e\u003cb\u003eJavaScript Mastery\u003c/b\u003e\u003c/a\u003e.\n\n## \u003ca name=\"tech-stack\"\u003e⚙️ Tech Stack\u003c/a\u003e\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\"\u003e🔋 Features\u003c/a\u003e\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\"\u003e🤸 Quick Start\u003c/a\u003e\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://git-scm.com/)\n- [Node.js](https://nodejs.org/en)\n- [npm](https://www.npmjs.com/) (Node Package Manager)\n\n**Cloning the Repository**\n\n```bash\ngit clone https://github.com/adrianhajdin/banking.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://cloud.appwrite.io/v1\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://api-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://appwrite.io/?utm_source=youtube\u0026utm_content=reactnative\u0026ref=JSmastery), [Plaid](https://plaid.com/) and [Dwolla](https://www.dwolla.com/)\n\n**Running the Project**\n\n```bash\nnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) in your browser to view the project.\n\n## \u003ca name=\"snippets\"\u003e🕸️ Snippets\u003c/a\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003e.env.example\u003c/code\u003e\u003c/summary\u003e\n\n```env\n#NEXT\nNEXT_PUBLIC_SITE_URL=\n\n#APPWRITE\nNEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1\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://api-sandbox.dwolla.com\nDWOLLA_ENV=sandbox\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eexchangePublicToken\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n// This function exchanges a public token for an access token and item ID\nexport const exchangePublicToken = async ({\n  publicToken,\n  user,\n}: exchangePublicTokenProps) =\u003e {\n  try {\n    // 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    // 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    // 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    // 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    // If the funding source URL is not created, throw an error\n    if (!fundingSourceUrl) throw Error;\n\n    // 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    // Revalidate the path to reflect the changes\n    revalidatePath(\"/\");\n\n    // Return a success message\n    return parseStringify({\n      publicTokenExchange: \"complete\",\n    });\n  } catch (error) {\n    // 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/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edwolla.actions.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use server\";\n\nimport { Client } from \"dwolla-v2\";\n\nconst getEnvironment = (): \"production\" | \"sandbox\" =\u003e {\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// Create a Dwolla Funding Source using a Plaid Processor Token\nexport const createFundingSource = async (\n  options: CreateFundingSourceOptions\n) =\u003e {\n  try {\n    return await dwollaClient\n      .post(`customers/${options.customerId}/funding-sources`, {\n        name: options.fundingSourceName,\n        plaidToken: options.plaidToken,\n      })\n      .then((res) =\u003e res.headers.get(\"location\"));\n  } catch (err) {\n    console.error(\"Creating a Funding Source Failed: \", err);\n  }\n};\n\nexport const createOnDemandAuthorization = async () =\u003e {\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) =\u003e {\n  try {\n    return await dwollaClient\n      .post(\"customers\", newCustomer)\n      .then((res) =\u003e 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) =\u003e {\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) =\u003e 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) =\u003e {\n  try {\n    // create dwolla auth link\n    const dwollaAuthLinks = await createOnDemandAuthorization();\n\n    // add funding source to the dwolla customer \u0026 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/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ebank.actions.ts\u003c/code\u003e\u003c/summary\u003e\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 \"../plaid.config\";\nimport { parseStringify } from \"../utils\";\n\nimport { getTransactionsByBankId } from \"./transaction.actions\";\nimport { getBanks, getBank } from \"./user.actions\";\n\n// Get multiple bank accounts\nexport const getAccounts = async ({ userId }: getAccountsProps) =\u003e {\n  try {\n    // get banks from db\n    const banks = await getBanks({ userId });\n\n    const accounts = await Promise.all(\n      banks?.map(async (bank: Bank) =\u003e {\n        // 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        // 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) =\u003e {\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// Get one bank account\nexport const getAccount = async ({ appwriteItemId }: getAccountProps) =\u003e {\n  try {\n    // get bank from db\n    const bank = await getBank({ documentId: appwriteItemId });\n\n    // 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    // 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) =\u003e ({\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    // 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    // sort transactions by date such that the most recent transaction is first\n    const allTransactions = [...transactions, ...transferTransactions].sort(\n      (a, b) =\u003e 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// Get bank info\nexport const getInstitution = async ({\n  institutionId,\n}: getInstitutionProps) =\u003e {\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// Get transactions\nexport const getTransactions = async ({\n  accessToken,\n}: getTransactionsProps) =\u003e {\n  let hasMore = true;\n  let transactions: any = [];\n\n  try {\n    // 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) =\u003e ({\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// Create Transfer\nexport const createTransfer = async () =\u003e {\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/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eBankTabItem.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport { useSearchParams, useRouter } from \"next/navigation\";\n\nimport { cn, formUrlQuery } from \"@/lib/utils\";\n\nexport const BankTabItem = ({ account, appwriteItemId }: BankTabItemProps) =\u003e {\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const isActive = appwriteItemId === account?.appwriteItemId;\n\n  const handleBankChange = () =\u003e {\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    \u003e\n      \u003cp\n        className={cn(`text-16 line-clamp-1 flex-1 font-medium text-gray-500`, {\n          \" text-blue-600\": isActive,\n        })}\n      \u003e\n        {account.name}\n      \u003c/p\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eBankInfo.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport Image from \"next/image\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\n\nimport {\n  cn,\n  formUrlQuery,\n  formatAmount,\n  getAccountTypeColors,\n} from \"@/lib/utils\";\n\nconst BankInfo = ({ account, appwriteItemId, type }: BankInfoProps) =\u003e {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n\n  const isActive = appwriteItemId === account?.appwriteItemId;\n\n  const handleBankChange = () =\u003e {\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\" \u0026\u0026 isActive,\n        \"rounded-xl\": type === \"card\",\n        \"hover:shadow-sm cursor-pointer\": type === \"card\",\n      })}\n    \u003e\n      \u003cfigure\n        className={`flex-center h-fit rounded-full bg-blue-100 ${colors.lightBg}`}\n      \u003e\n        \u003cImage\n          src=\"/icons/connect-bank.svg\"\n          width={20}\n          height={20}\n          alt={account.subtype}\n          className=\"m-2 min-w-5\"\n        /\u003e\n      \u003c/figure\u003e\n      \u003cdiv className=\"flex w-full flex-1 flex-col justify-center gap-1\"\u003e\n        \u003cdiv className=\"bank-info_content\"\u003e\n          \u003ch2\n            className={`text-16 line-clamp-1 flex-1 font-bold text-blue-900 ${colors.title}`}\n          \u003e\n            {account.name}\n          \u003c/h2\u003e\n          {type === \"full\" \u0026\u0026 (\n            \u003cp\n              className={`text-12 rounded-full px-3 py-1 font-medium text-blue-700 ${colors.subText} ${colors.lightBg}`}\n            \u003e\n              {account.subtype}\n            \u003c/p\u003e\n          )}\n        \u003c/div\u003e\n\n        \u003cp className={`text-16 font-medium text-blue-700 ${colors.subText}`}\u003e\n          {formatAmount(account.currentBalance)}\n        \u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n\nexport default BankInfo;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eCopy.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\nimport { useState } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\nconst Copy = ({ title }: { title: string }) =\u003e {\n  const [hasCopied, setHasCopied] = useState(false);\n\n  const copyToClipboard = () =\u003e {\n    navigator.clipboard.writeText(title);\n    setHasCopied(true);\n\n    setTimeout(() =\u003e {\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    \u003e\n      \u003cp className=\"line-clamp-1 w-full max-w-full text-xs font-medium text-black-2\"\u003e\n        {title}\n      \u003c/p\u003e\n\n      {!hasCopied ? (\n        \u003csvg\n          xmlns=\"http://www.w3.org/2000/svg\"\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        \u003e\n          \u003crect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"\u003e\u003c/rect\u003e\n          \u003cpath d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"\u003e\u003c/path\u003e\n        \u003c/svg\u003e\n      ) : (\n        \u003csvg\n          xmlns=\"http://www.w3.org/2000/svg\"\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        \u003e\n          \u003cpolyline points=\"20 6 9 17 4 12\"\u003e\u003c/polyline\u003e\n        \u003c/svg\u003e\n      )}\n    \u003c/Button\u003e\n  );\n};\n\nexport default Copy;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ePaymentTransferForm.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Loader2 } from \"lucide-react\";\nimport { useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport * as z from \"zod\";\n\nimport { createTransfer } from \"@/lib/actions/dwolla.actions\";\nimport { createTransaction } from \"@/lib/actions/transaction.actions\";\nimport { getBank, getBankByAccountId } from \"@/lib/actions/user.actions\";\nimport { decryptId } from \"@/lib/utils\";\n\nimport { BankDropdown } from \"./bank/BankDropdown\";\nimport { Button } from \"./ui/button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"./ui/form\";\nimport { Input } from \"./ui/input\";\nimport { Textarea } from \"./ui/textarea\";\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) =\u003e {\n  const router = useRouter();\n  const [isLoading, setIsLoading] = useState(false);\n\n  const form = useForm\u003cz.infer\u003ctypeof formSchema\u003e\u003e({\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\u003e) =\u003e {\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      // create transfer\n      const transfer = await createTransfer(transferParams);\n\n      // 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(\"/\");\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}\u003e\n      \u003cform onSubmit={form.handleSubmit(submit)} className=\"flex flex-col\"\u003e\n        \u003cFormField\n          control={form.control}\n          name=\"senderBank\"\n          render={() =\u003e (\n            \u003cFormItem className=\"border-t border-gray-200\"\u003e\n              \u003cdiv className=\"payment-transfer_form-item pb-6 pt-5\"\u003e\n                \u003cdiv className=\"payment-transfer_form-content\"\u003e\n                  \u003cFormLabel className=\"text-14 font-medium text-gray-700\"\u003e\n                    Select Source Bank\n                  \u003c/FormLabel\u003e\n                  \u003cFormDescription className=\"text-12 font-normal text-gray-600\"\u003e\n                    Select the bank account you want to transfer funds from\n                  \u003c/FormDescription\u003e\n                \u003c/div\u003e\n                \u003cdiv className=\"flex w-full flex-col\"\u003e\n                  \u003cFormControl\u003e\n                    \u003cBankDropdown\n                      accounts={accounts}\n                      setValue={form.setValue}\n                      otherStyles=\"!w-full\"\n                    /\u003e\n                  \u003c/FormControl\u003e\n                  \u003cFormMessage className=\"text-12 text-red-500\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            \u003c/FormItem\u003e\n          )}\n        /\u003e\n\n        \u003cFormField\n          control={form.control}\n          name=\"name\"\n          render={({ field }) =\u003e (\n            \u003cFormItem className=\"border-t border-gray-200\"\u003e\n              \u003cdiv className=\"payment-transfer_form-item pb-6 pt-5\"\u003e\n                \u003cdiv className=\"payment-transfer_form-content\"\u003e\n                  \u003cFormLabel className=\"text-14 font-medium text-gray-700\"\u003e\n                    Transfer Note (Optional)\n                  \u003c/FormLabel\u003e\n                  \u003cFormDescription className=\"text-12 font-normal text-gray-600\"\u003e\n                    Please provide any additional information or instructions\n                    related to the transfer\n                  \u003c/FormDescription\u003e\n                \u003c/div\u003e\n                \u003cdiv className=\"flex w-full flex-col\"\u003e\n                  \u003cFormControl\u003e\n                    \u003cTextarea\n                      placeholder=\"Write a short note here\"\n                      className=\"input-class\"\n                      {...field}\n                    /\u003e\n                  \u003c/FormControl\u003e\n                  \u003cFormMessage className=\"text-12 text-red-500\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            \u003c/FormItem\u003e\n          )}\n        /\u003e\n\n        \u003cdiv className=\"payment-transfer_form-details\"\u003e\n          \u003ch2 className=\"text-18 font-semibold text-gray-900\"\u003e\n            Bank account details\n          \u003c/h2\u003e\n          \u003cp className=\"text-16 font-normal text-gray-600\"\u003e\n            Enter the bank account details of the recipient\n          \u003c/p\u003e\n        \u003c/div\u003e\n\n        \u003cFormField\n          control={form.control}\n          name=\"email\"\n          render={({ field }) =\u003e (\n            \u003cFormItem className=\"border-t border-gray-200\"\u003e\n              \u003cdiv className=\"payment-transfer_form-item py-5\"\u003e\n                \u003cFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\"\u003e\n                  Recipient\u0026apos;s Email Address\n                \u003c/FormLabel\u003e\n                \u003cdiv className=\"flex w-full flex-col\"\u003e\n                  \u003cFormControl\u003e\n                    \u003cInput\n                      placeholder=\"ex: johndoe@gmail.com\"\n                      className=\"input-class\"\n                      {...field}\n                    /\u003e\n                  \u003c/FormControl\u003e\n                  \u003cFormMessage className=\"text-12 text-red-500\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            \u003c/FormItem\u003e\n          )}\n        /\u003e\n\n        \u003cFormField\n          control={form.control}\n          name=\"sharableId\"\n          render={({ field }) =\u003e (\n            \u003cFormItem className=\"border-t border-gray-200\"\u003e\n              \u003cdiv className=\"payment-transfer_form-item pb-5 pt-6\"\u003e\n                \u003cFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\"\u003e\n                  Receiver\u0026apos;s Plaid Sharable Id\n                \u003c/FormLabel\u003e\n                \u003cdiv className=\"flex w-full flex-col\"\u003e\n                  \u003cFormControl\u003e\n                    \u003cInput\n                      placeholder=\"Enter the public account number\"\n                      className=\"input-class\"\n                      {...field}\n                    /\u003e\n                  \u003c/FormControl\u003e\n                  \u003cFormMessage className=\"text-12 text-red-500\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            \u003c/FormItem\u003e\n          )}\n        /\u003e\n\n        \u003cFormField\n          control={form.control}\n          name=\"amount\"\n          render={({ field }) =\u003e (\n            \u003cFormItem className=\"border-y border-gray-200\"\u003e\n              \u003cdiv className=\"payment-transfer_form-item py-5\"\u003e\n                \u003cFormLabel className=\"text-14 w-full max-w-[280px] font-medium text-gray-700\"\u003e\n                  Amount\n                \u003c/FormLabel\u003e\n                \u003cdiv className=\"flex w-full flex-col\"\u003e\n                  \u003cFormControl\u003e\n                    \u003cInput\n                      placeholder=\"ex: 5.00\"\n                      className=\"input-class\"\n                      {...field}\n                    /\u003e\n                  \u003c/FormControl\u003e\n                  \u003cFormMessage className=\"text-12 text-red-500\" /\u003e\n                \u003c/div\u003e\n              \u003c/div\u003e\n            \u003c/FormItem\u003e\n          )}\n        /\u003e\n\n        \u003cdiv className=\"payment-transfer_btn-box\"\u003e\n          \u003cButton type=\"submit\" className=\"payment-transfer_btn\"\u003e\n            {isLoading ? (\n              \u003c\u003e\n                \u003cLoader2 size={20} className=\"animate-spin\" /\u003e \u0026nbsp; Sending...\n              \u003c/\u003e\n            ) : (\n              \"Transfer Funds\"\n            )}\n          \u003c/Button\u003e\n        \u003c/div\u003e\n      \u003c/form\u003e\n    \u003c/Form\u003e\n  );\n};\n\nexport default PaymentTransferForm;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ePagination.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport Image from \"next/image\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { formUrlQuery } from \"@/lib/utils\";\n\nexport const Pagination = ({ page, totalPages }: PaginationProps) =\u003e {\n  const router = useRouter();\n  const searchParams = useSearchParams()!;\n\n  const handleNavigation = (type: \"prev\" | \"next\") =\u003e {\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\"\u003e\n      \u003cButton\n        size=\"lg\"\n        variant=\"ghost\"\n        className=\"p-0 hover:bg-transparent\"\n        onClick={() =\u003e handleNavigation(\"prev\")}\n        disabled={Number(page) \u003c= 1}\n      \u003e\n        \u003cImage\n          src=\"/icons/arrow-left.svg\"\n          alt=\"arrow\"\n          width={20}\n          height={20}\n          className=\"mr-2\"\n        /\u003e\n        Prev\n      \u003c/Button\u003e\n      \u003cp className=\"text-14 flex items-center px-2\"\u003e\n        {page} / {totalPages}\n      \u003c/p\u003e\n      \u003cButton\n        size=\"lg\"\n        variant=\"ghost\"\n        className=\"p-0 hover:bg-transparent\"\n        onClick={() =\u003e handleNavigation(\"next\")}\n        disabled={Number(page) \u003e= totalPages}\n      \u003e\n        Next\n        \u003cImage\n          src=\"/icons/arrow-left.svg\"\n          alt=\"arrow\"\n          width={20}\n          height={20}\n          className=\"ml-2 -scale-x-100\"\n        /\u003e\n      \u003c/Button\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eCategory.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport Image from \"next/image\";\n\nimport { topCategoryStyles } from \"@/constants\";\nimport { cn } from \"@/lib/utils\";\n\nimport { Progress } from \"./ui/progress\";\n\nexport const Category = ({ category }: CategoryProps) =\u003e {\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)}\u003e\n      \u003cfigure className={cn(\"flex-center size-10 rounded-full\", circleBg)}\u003e\n        \u003cImage src={icon} width={20} height={20} alt={category.name} /\u003e\n      \u003c/figure\u003e\n      \u003cdiv className=\"flex w-full flex-1 flex-col gap-2\"\u003e\n        \u003cdiv className=\"text-14 flex justify-between\"\u003e\n          \u003ch2 className={cn(\"font-medium\", main)}\u003e{category.name}\u003c/h2\u003e\n          \u003ch3 className={cn(\"font-normal\", count)}\u003e{category.count}\u003c/h3\u003e\n        \u003c/div\u003e\n        \u003cProgress\n          value={(category.count / category.totalCount) * 100}\n          className={cn(\"h-2 w-full\", progressBg)}\n          indicatorClassName={cn(\"h-2 w-full\", indicator)}\n        /\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n## \u003ca name=\"links\"\u003e🔗 Links\u003c/a\u003e\n\nAssets used in the project can be found [here](https://drive.google.com/file/d/1_qdx0YGJHIKaGvnAhB_ZjKON5lMZ8JEa/view?usp=sharing)\n\n## \u003ca name=\"more\"\u003e🚀 More\u003c/a\u003e\n\n**Advance your skills with Next.js 14 Pro Course**\n\nEnjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!\n\n\u003ca href=\"https://jsmastery.pro/next14\" target=\"_blank\"\u003e\n\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/557837ce-f612-4530-ab24-189e75133c71\" alt=\"Project Banner\"\u003e\n\u003c/a\u003e\n\n\u003cbr /\u003e\n\u003cbr /\u003e\n\n**Accelerate your professional journey with the Expert Training program**\n\nAnd if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together!\n\n\u003ca href=\"https://www.jsmastery.pro/masterclass\" target=\"_blank\"\u003e\n\u003cimg src=\"https://github.com/sujatagunale/EasyRead/assets/151519281/fed352ad-f27b-400d-9b8f-c7fe628acb84\" alt=\"Project Banner\"\u003e\n\u003c/a\u003e\n\n#\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyann1294%2Fbanking","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyann1294%2Fbanking","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyann1294%2Fbanking/lists"}