12 |
13 |
14 | {user.name[0]}
15 |
16 |
17 |
18 | {user.name}
19 |
20 |
21 | {user.email}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
My Banks
29 |
30 |
36 | Add Bank
37 |
38 |
39 | {banks?.length > 0 && (
40 |
41 |
42 |
48 |
49 | {banks[1] && (
50 |
51 |
57 |
58 | )}
59 |
60 | )}
61 |
62 |
63 | )
64 | }
65 |
66 | export default RightSidebar
--------------------------------------------------------------------------------
/components/MobileNav.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import Image from 'next/image'
4 | import {
5 | Sheet,
6 | SheetClose,
7 | SheetContent,
8 | SheetDescription,
9 | SheetHeader,
10 | SheetTitle,
11 | SheetTrigger,
12 | } from "@/components/ui/sheet"
13 | import Link from 'next/link'
14 | import { sidebarLinks } from '@/constants'
15 | import { usePathname } from 'next/navigation'
16 | import { cn } from '@/lib/utils'
17 | import Footer from './Footer'
18 |
19 | const MobileNav = ({user} : MobileNavProps) => {
20 | const pathname = usePathname();
21 | return (
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
41 | Horizon
42 |
43 |
44 |
45 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
79 | export default MobileNav
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import {withSentryConfig} from "@sentry/nextjs";
2 | import type { NextConfig } from "next";
3 |
4 | const nextConfig: NextConfig = {
5 | /* config options here */
6 | };
7 |
8 | export default withSentryConfig(withSentryConfig(nextConfig, {
9 | // For all available options, see:
10 | // https://github.com/getsentry/sentry-webpack-plugin#options
11 |
12 | org: "navin-zy",
13 | project: "javascript-nextjs",
14 |
15 | // Only print logs for uploading source maps in CI
16 | silent: !process.env.CI,
17 |
18 | // For all available options, see:
19 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
20 |
21 | // Upload a larger set of source maps for prettier stack traces (increases build time)
22 | widenClientFileUpload: true,
23 |
24 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
25 | // This can increase your server load as well as your hosting bill.
26 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
27 | // side errors will fail.
28 | // tunnelRoute: "/monitoring",
29 |
30 | // Hides source maps from generated client bundles
31 | hideSourceMaps: true,
32 |
33 | // Automatically tree-shake Sentry logger statements to reduce bundle size
34 | disableLogger: true,
35 |
36 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
37 | // See the following for more information:
38 | // https://docs.sentry.io/product/crons/
39 | // https://vercel.com/docs/cron-jobs
40 | automaticVercelMonitors: true,
41 | }), {
42 | // For all available options, see:
43 | // https://github.com/getsentry/sentry-webpack-plugin#options
44 |
45 | org: "navin-zy",
46 | project: "javascript-nextjs",
47 |
48 | // Only print logs for uploading source maps in CI
49 | silent: !process.env.CI,
50 |
51 | // For all available options, see:
52 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
53 |
54 | // Upload a larger set of source maps for prettier stack traces (increases build time)
55 | widenClientFileUpload: true,
56 |
57 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
58 | // This can increase your server load as well as your hosting bill.
59 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
60 | // side errors will fail.
61 | // tunnelRoute: "/monitoring",
62 |
63 | // Hides source maps from generated client bundles
64 | hideSourceMaps: true,
65 |
66 | // Automatically tree-shake Sentry logger statements to reduce bundle size
67 | disableLogger: true,
68 |
69 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
70 | // See the following for more information:
71 | // https://docs.sentry.io/product/crons/
72 | // https://vercel.com/docs/cron-jobs
73 | automaticVercelMonitors: true,
74 | });
75 | module.exports = {
76 | reactStrictMode: true,
77 | devIndicators: {
78 | autoPrerender: false,
79 | },
80 | };
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | "./constants/**/*.{ts,tsx}",
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1400px",
19 | },
20 | },
21 | extend: {
22 | colors: {
23 | fill: {
24 | 1: "rgba(255, 255, 255, 0.10)",
25 | },
26 | bankGradient: "#0179FE",
27 | indigo: {
28 | 500: "#6172F3",
29 | 700: "#3538CD",
30 | },
31 | success: {
32 | 25: "#F6FEF9",
33 | 50: "#ECFDF3",
34 | 100: "#D1FADF",
35 | 600: "#039855",
36 | 700: "#027A48",
37 | 900: "#054F31",
38 | },
39 | pink: {
40 | 25: "#FEF6FB",
41 | 100: "#FCE7F6",
42 | 500: "#EE46BC",
43 | 600: "#DD2590",
44 | 700: "#C11574",
45 | 900: "#851651",
46 | },
47 | blue: {
48 | 25: "#F5FAFF",
49 | 100: "#D1E9FF",
50 | 500: "#2E90FA",
51 | 600: "#1570EF",
52 | 700: "#175CD3",
53 | 900: "#194185",
54 | },
55 | sky: {
56 | 1: "#F3F9FF",
57 | },
58 | black: {
59 | 1: "#00214F",
60 | 2: "#344054",
61 | },
62 | gray: {
63 | 25: "#FCFCFD",
64 | 200: "#EAECF0",
65 | 300: "#D0D5DD",
66 | 500: "#667085",
67 | 600: "#475467",
68 | 700: "#344054",
69 | 900: "#101828",
70 | },
71 | },
72 | backgroundImage: {
73 | "bank-gradient": "linear-gradient(90deg, #0179FE 0%, #4893FF 100%)",
74 | "gradient-mesh": "url('/icons/gradient-mesh.svg')",
75 | "bank-green-gradient":
76 | "linear-gradient(90deg, #01797A 0%, #489399 100%)",
77 | },
78 | boxShadow: {
79 | form: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
80 | chart:
81 | "0px 1px 3px 0px rgba(16, 24, 40, 0.10), 0px 1px 2px 0px rgba(16, 24, 40, 0.06)",
82 | profile:
83 | "0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)",
84 | creditCard: "8px 10px 16px 0px rgba(0, 0, 0, 0.05)",
85 | },
86 | fontFamily: {
87 | inter: "var(--font-inter)",
88 | "ibm-plex-serif": "var(--font-ibm-plex-serif)",
89 | },
90 | keyframes: {
91 | "accordion-down": {
92 | from: { height: "0" },
93 | to: { height: "var(--radix-accordion-content-height)" },
94 | },
95 | "accordion-up": {
96 | from: { height: "var(--radix-accordion-content-height)" },
97 | to: { height: "0" },
98 | },
99 | },
100 | animation: {
101 | "accordion-down": "accordion-down 0.2s ease-out",
102 | "accordion-up": "accordion-up 0.2s ease-out",
103 | },
104 | },
105 | },
106 | plugins: [require("tailwindcss-animate")],
107 | } satisfies Config;
108 |
109 | export default config;
110 |
--------------------------------------------------------------------------------
/lib/actions/dwolla.actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Client } from "dwolla-v2";
4 |
5 | const getEnvironment = (): "production" | "sandbox" => {
6 | const environment = process.env.DWOLLA_ENV as string;
7 |
8 | switch (environment) {
9 | case "sandbox":
10 | return "sandbox";
11 | case "production":
12 | return "production";
13 | default:
14 | throw new Error(
15 | "Dwolla environment should either be set to `sandbox` or `production`"
16 | );
17 | }
18 | };
19 | const appKey = "nm6D9mZoZMsR3iM3eBRQcfL0euonEsV8kKvwppD7yfEec6S7QB"
20 | const appSecret = "oIHpEl7ZWucyHURnJR1xtN5jiuRSAZc2mFmtf7tih4RZcawfkr"
21 | const dwollaClient = new Client({
22 |
23 | environment: getEnvironment(),
24 | key: appKey,
25 | secret: appSecret,
26 | });
27 |
28 | // Create a Dwolla Funding Source using a Plaid Processor Token
29 | export const createFundingSource = async (
30 | options: CreateFundingSourceOptions
31 | ) => {
32 | try {
33 | return await dwollaClient
34 | .post(`customers/${options.customerId}/funding-sources`, {
35 | name: options.fundingSourceName,
36 | plaidToken: options.plaidToken,
37 | })
38 | .then((res) => res.headers.get("location"));
39 | } catch (err) {
40 | console.error("Creating a Funding Source Failed: ", err);
41 | }
42 | };
43 |
44 | export const createOnDemandAuthorization = async () => {
45 | try {
46 | const onDemandAuthorization = await dwollaClient.post(
47 | "on-demand-authorizations"
48 | );
49 | const authLink = onDemandAuthorization.body._links;
50 | return authLink;
51 | } catch (err) {
52 | console.error("Creating an On Demand Authorization Failed: ", err);
53 | }
54 | };
55 |
56 | export const createDwollaCustomer = async (
57 | newCustomer: NewDwollaCustomerParams
58 | ) => {
59 | try {
60 | return await dwollaClient.post("customers", newCustomer)
61 | .then((res) => res.headers.get("location"));
62 |
63 | } catch (err) {
64 | console.error("Creating a Dwolla Customer Failed: ", err);
65 | }
66 | };
67 |
68 | export const createTransfer = async ({
69 | sourceFundingSourceUrl,
70 | destinationFundingSourceUrl,
71 | amount,
72 | }: TransferParams) => {
73 | try {
74 | const requestBody = {
75 | _links: {
76 | source: {
77 | href: sourceFundingSourceUrl,
78 | },
79 | destination: {
80 | href: destinationFundingSourceUrl,
81 | },
82 | },
83 | amount: {
84 | currency: "INR",
85 | value: amount,
86 | },
87 | };
88 | return await dwollaClient
89 | .post("transfers", requestBody)
90 | .then((res) => res.headers.get("location"));
91 | } catch (err) {
92 | console.error("Transfer fund failed: ", err);
93 | }
94 | };
95 |
96 | export const addFundingSource = async ({
97 | dwollaCustomerId,
98 | processorToken,
99 | bankName,
100 | }: AddFundingSourceParams) => {
101 | try {
102 | // create dwolla auth link
103 | const dwollaAuthLinks = await createOnDemandAuthorization();
104 |
105 | // add funding source to the dwolla customer & get the funding source url
106 | const fundingSourceOptions = {
107 | customerId: dwollaCustomerId,
108 | fundingSourceName: bankName,
109 | plaidToken: processorToken,
110 | _links: dwollaAuthLinks,
111 | };
112 | return await createFundingSource(fundingSourceOptions);
113 | } catch (err) {
114 | console.error("Transfer fund failed: ", err);
115 | }
116 | };
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const sidebarLinks = [
2 | {
3 | imgURL: "/icons/home.svg",
4 | route: "/",
5 | label: "Home",
6 | },
7 | {
8 | imgURL: "/icons/dollar-circle.svg",
9 | route: "/my-banks",
10 | label: "My Banks",
11 | },
12 | {
13 | imgURL: "/icons/transaction.svg",
14 | route: "/transaction-history",
15 | label: "Transaction History",
16 | },
17 | {
18 | imgURL: "/icons/money-send.svg",
19 | route: "/payment-transfer",
20 | label: "Transfer Funds",
21 | },
22 | ];
23 |
24 | // good_user / good_password - Bank of America
25 | export const TEST_USER_ID = "6627ed3d00267aa6fa3e";
26 |
27 | // custom_user -> Chase Bank
28 | // export const TEST_ACCESS_TOKEN =
29 | // "access-sandbox-da44dac8-7d31-4f66-ab36-2238d63a3017";
30 |
31 | // custom_user -> Chase Bank
32 | export const TEST_ACCESS_TOKEN =
33 | "access-sandbox-229476cf-25bc-46d2-9ed5-fba9df7a5d63";
34 |
35 | export const ITEMS = [
36 | {
37 | id: "6624c02e00367128945e", // appwrite item Id
38 | accessToken: "access-sandbox-83fd9200-0165-4ef8-afde-65744b9d1548",
39 | itemId: "VPMQJKG5vASvpX8B6JK3HmXkZlAyplhW3r9xm",
40 | userId: "6627ed3d00267aa6fa3e",
41 | accountId: "X7LMJkE5vnskJBxwPeXaUWDBxAyZXwi9DNEWJ",
42 | },
43 | {
44 | id: "6627f07b00348f242ea9", // appwrite item Id
45 | accessToken: "access-sandbox-74d49e15-fc3b-4d10-a5e7-be4ddae05b30",
46 | itemId: "Wv7P6vNXRXiMkoKWPzeZS9Zm5JGWdXulLRNBq",
47 | userId: "6627ed3d00267aa6fa3e",
48 | accountId: "x1GQb1lDrDHWX4BwkqQbI4qpQP1lL6tJ3VVo9",
49 | },
50 | ];
51 |
52 | export const topCategoryStyles = {
53 | "Food and Drink": {
54 | bg: "bg-blue-25",
55 | circleBg: "bg-blue-100",
56 | text: {
57 | main: "text-blue-900",
58 | count: "text-blue-700",
59 | },
60 | progress: {
61 | bg: "bg-blue-100",
62 | indicator: "bg-blue-700",
63 | },
64 | icon: "/icons/monitor.svg",
65 | },
66 | Travel: {
67 | bg: "bg-success-25",
68 | circleBg: "bg-success-100",
69 | text: {
70 | main: "text-success-900",
71 | count: "text-success-700",
72 | },
73 | progress: {
74 | bg: "bg-success-100",
75 | indicator: "bg-success-700",
76 | },
77 | icon: "/icons/coins.svg",
78 | },
79 | default: {
80 | bg: "bg-pink-25",
81 | circleBg: "bg-pink-100",
82 | text: {
83 | main: "text-pink-900",
84 | count: "text-pink-700",
85 | },
86 | progress: {
87 | bg: "bg-pink-100",
88 | indicator: "bg-pink-700",
89 | },
90 | icon: "/icons/shopping-bag.svg",
91 | },
92 | };
93 |
94 | export const transactionCategoryStyles = {
95 | "Food and Drink": {
96 | borderColor: "border-pink-600",
97 | backgroundColor: "bg-pink-500",
98 | textColor: "text-pink-700",
99 | chipBackgroundColor: "bg-inherit",
100 | },
101 | Payment: {
102 | borderColor: "border-success-600",
103 | backgroundColor: "bg-green-600",
104 | textColor: "text-success-700",
105 | chipBackgroundColor: "bg-inherit",
106 | },
107 | "Bank Fees": {
108 | borderColor: "border-success-600",
109 | backgroundColor: "bg-green-600",
110 | textColor: "text-success-700",
111 | chipBackgroundColor: "bg-inherit",
112 | },
113 | Transfer: {
114 | borderColor: "border-red-700",
115 | backgroundColor: "bg-red-700",
116 | textColor: "text-red-700",
117 | chipBackgroundColor: "bg-inherit",
118 | },
119 | Processing: {
120 | borderColor: "border-[#F2F4F7]",
121 | backgroundColor: "bg-gray-500",
122 | textColor: "text-[#344054]",
123 | chipBackgroundColor: "bg-[#F2F4F7]",
124 | },
125 | Success: {
126 | borderColor: "border-[#12B76A]",
127 | backgroundColor: "bg-[#12B76A]",
128 | textColor: "text-[#027A48]",
129 | chipBackgroundColor: "bg-[#ECFDF3]",
130 | },
131 | default: {
132 | borderColor: "",
133 | backgroundColor: "bg-blue-500",
134 | textColor: "text-blue-700",
135 | chipBackgroundColor: "bg-inherit",
136 | },
137 | };
138 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef
,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/public/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/AuthForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import React, { useState } from 'react'
4 | import Image from 'next/image'
5 |
6 |
7 | import { z } from "zod"
8 | import { zodResolver } from "@hookform/resolvers/zod"
9 | import { useForm } from "react-hook-form"
10 | import { Button } from "@/components/ui/button"
11 | import {
12 | Form,
13 | FormControl,
14 | FormDescription,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form"
20 | import { Input } from "@/components/ui/input"
21 | import Custominput from './Custominput'
22 | import { AuthFormSchema } from '@/lib/utils'
23 | import { Loader2 } from 'lucide-react'
24 | import { useRouter } from 'next/navigation'
25 | import { signIn, signUp } from '@/lib/actions/user.actions'
26 | import PlaidLink from './PlaidLink'
27 |
28 |
29 | const AuthForm = ({type} :{type : string} ) => {
30 | const router = useRouter();
31 | const [user, setuser] = useState(null);
32 | const [isLoading, setisLoading] = useState(false);
33 |
34 | const formSchema = AuthFormSchema(type);
35 | const form = useForm>({
36 | resolver: zodResolver(formSchema),
37 | defaultValues: {
38 | email: "",
39 | password:""
40 | },
41 | })
42 |
43 | // 2. Define a submit handler.
44 | const onSubmit = async (data: z.infer) => {
45 | // Do something with the form values.
46 | // ✅ This will be type-safe and validated.
47 | setisLoading(true);
48 | try {
49 | // let PostalCode;
50 | // function isValidIndianPostalCode(postalCode: string): boolean {
51 | // const indianPostalCodeRegex = /^[1-9][0-9]{5}$/;
52 | // return indianPostalCodeRegex.test(postalCode.trim());
53 | // }
54 |
55 |
56 | const userData = {
57 | firstName : data.firstName!,
58 | lastName : data.lastName!,
59 | address1 : data.address1!,
60 | city:data.city!,
61 | state:data.state!,
62 | postalCode:data.postalCode!,
63 | dateOfBirth:data.dateOfBirth!,
64 |
65 | ssn:data.ssn!,
66 | email:data.email,
67 | password:data.password
68 |
69 | }
70 | if(type === 'sign-up'){
71 | const newUser = await signUp(userData);
72 | setuser(newUser);
73 | }
74 | if(type==='sign-in'){
75 | const response = await signIn({
76 | email:data.email,
77 | password:data.password
78 | })
79 | if(response) router.push('/');
80 | }
81 | } catch (error) {
82 | console.log(error)
83 | } finally{
84 | setisLoading(false);
85 | }
86 | }
87 |
88 | return (
89 |
90 |
118 | {user ? (
119 |
122 | ):(
123 | <>
124 |
157 |
158 |
166 | >
167 | )}
168 |
169 | )
170 | }
171 |
172 | export default AuthForm
--------------------------------------------------------------------------------
/lib/actions/user.actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { ID } from "node-appwrite";
4 | import { createAdminClient, createSessionClient } from "../appwrite";
5 | import { cookies } from "next/headers";
6 | import { encryptId, extractCustomerIdFromUrl, parseStringify } from "../utils";
7 | import { CountryCode, ProcessorTokenCreateRequest, ProcessorTokenCreateRequestProcessorEnum, Products } from "plaid";
8 | import { Languages } from "lucide-react";
9 | import { plaidClient } from "@/lib/plaid";
10 | import { revalidatePath } from "next/cache";
11 | import { addFundingSource, createDwollaCustomer } from "./dwolla.actions";
12 | const {
13 | APPWRITE_DATABASE_ID : DATABASE_ID,
14 | APPWRITE_USER_COLLECTION_ID : USER_COLLECTION_ID,
15 | APPWRITE_BANK_COLLECTION_ID : BANK_COLLECTION_ID,
16 |
17 | } = process.env;
18 |
19 | export const signIn = async({email , password} :signInProps)=>{
20 | try {
21 | const { account } = await createSessionClient();
22 | const response = account.createEmailPasswordSession(email , password);
23 | return parseStringify(response)
24 |
25 |
26 | } catch (error) {
27 | console.error('Error' , error)
28 | }
29 | }
30 | export const signUp = async({ password , ...userData}: SignUpParams)=>{
31 | const {email , firstName , lastName} = userData;
32 | let newUserAccount;
33 | try {
34 | const { account , database} = await createAdminClient();
35 |
36 | newUserAccount = await account.create(
37 | ID.unique(),
38 | email,
39 | password,
40 | `${firstName} ${lastName}`
41 | );
42 | if(!newUserAccount) throw new Error('Error Creating User');
43 | const dwollaCustomerUrl = await createDwollaCustomer({
44 | ...userData,
45 | type: 'personal'
46 | })
47 | if(!dwollaCustomerUrl) throw new Error('Error Createing Dwolla customer');
48 | const dwollaCustomerId = extractCustomerIdFromUrl(dwollaCustomerUrl);
49 | const newUser = await database.createDocument(
50 | DATABASE_ID!,
51 | USER_COLLECTION_ID!,
52 | ID.unique(),
53 | {
54 | ...userData,
55 | userId:newUserAccount.$id,
56 | dwollaCustomerId,
57 | dwollaCustomerUrl
58 | }
59 | )
60 | const session = await account.createEmailPasswordSession(email, password);
61 |
62 | (await cookies()).set("appwrite-session", session.secret, {
63 | path: "/",
64 | httpOnly: true,
65 | sameSite: "strict",
66 | secure: true,
67 | });
68 | return parseStringify(newUser);
69 | } catch (error) {
70 | console.error('Error' , error)
71 | }
72 | }
73 | // ... your initilization functions
74 |
75 | export async function getLoggedInUser() {
76 | try {
77 | const { account } = await createSessionClient();
78 | const user = await account.get();
79 | return parseStringify(user)
80 | } catch (error) {
81 | return null;
82 | }
83 | }
84 | export async function logoutAccount(){
85 | try {
86 | const { account } = await createSessionClient();
87 | (await cookies()).delete("appwrite-session");
88 | await account.deleteSession('current');
89 |
90 | } catch (error) {
91 | return null;
92 | }
93 | }
94 |
95 | export const createLinkToken = async(user :User)=>{
96 | try {
97 | const tokenParams = {
98 | user:{
99 | client_user_id: user.$id,
100 |
101 | },
102 | client_name: `${user.firstName} ${user.lastName}`,
103 | products: ['auth'] as Products[],
104 | language:'en',
105 | country_codes:['US'] as CountryCode[],
106 |
107 | }
108 | const response = await plaidClient.linkTokenCreate(tokenParams);
109 | return parseStringify({linkToken : response.data.link_token})
110 | } catch (error) {
111 | console.log(error)
112 | }
113 | }
114 | export const createBankAccount = async({
115 | userId,
116 | bankId,
117 | accountId,
118 | accessToken,
119 | fundingSourceUrl,
120 | sharableId,
121 |
122 | } : createBankAccountProps)=>{
123 | try {
124 | const {database } = await createAdminClient();
125 | const bankAccount = await database.createDocument(
126 | DATABASE_ID!,
127 | BANK_COLLECTION_ID!,
128 | ID.unique(),
129 | {
130 | userId,
131 | bankId,
132 | accountId,
133 | accessToken,
134 | fundingSourceUrl,
135 | sharableId,
136 | }
137 |
138 | )
139 | return parseStringify(bankAccount)
140 | } catch (error) {
141 |
142 | }
143 | }
144 | export const exchangePublicToken = async ({publicToken , user ,} :exchangePublicTokenProps)=>{
145 | try {
146 | // Exchange public token for access token and item ID
147 | const response = await plaidClient.itemPublicTokenExchange({
148 | public_token: publicToken,
149 | });
150 | const accessToken = response.data.access_token;
151 | const itemId = response.data.item_id;
152 |
153 | // Get account information from Plaid using the access token
154 | const accountsResponse = await plaidClient.accountsGet({
155 | access_token: accessToken,
156 | });
157 | const accountData = accountsResponse.data.accounts[0];
158 | // Create a processor token for Dwolla using the access token and account ID
159 | const request: ProcessorTokenCreateRequest = {
160 | access_token: accessToken,
161 | account_id: accountData.account_id,
162 | processor: "dwolla" as ProcessorTokenCreateRequestProcessorEnum,
163 | };
164 | const processorTokenResponse = await plaidClient.processorTokenCreate(request);
165 | const processorToken = processorTokenResponse.data.processor_token;
166 | // Create a funding source URL for the account using the Dwolla customer ID, processor token, and bank name
167 | const fundingSourceUrl = await addFundingSource({
168 | dwollaCustomerId: user.dwollaCustomerId,
169 | processorToken,
170 | bankName: accountData.name,
171 | });
172 | if (!fundingSourceUrl) throw Error;
173 |
174 | // Create a bank account using the user ID, item ID, account ID, access token, funding source URL, and sharable ID
175 | await createBankAccount({
176 | userId: user.$id,
177 | bankId: itemId,
178 | accountId: accountData.account_id,
179 | accessToken,
180 | fundingSourceUrl,
181 | sharableId: encryptId(accountData.account_id),
182 | });
183 | revalidatePath("/");
184 | return parseStringify({
185 | publicTokenExchange: "complete",
186 | });
187 |
188 | } catch (error) {
189 | console.error("An error occurred while creating exchanging token:", error);
190 | }
191 | }
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-prototype-builtins */
2 | import { type ClassValue, clsx } from "clsx";
3 | import qs from "query-string";
4 | import { twMerge } from "tailwind-merge";
5 | import { z } from "zod";
6 |
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs));
9 | }
10 |
11 | // FORMAT DATE TIME
12 | export const formatDateTime = (dateString: Date) => {
13 | const dateTimeOptions: Intl.DateTimeFormatOptions = {
14 | weekday: "short", // abbreviated weekday name (e.g., 'Mon')
15 | month: "short", // abbreviated month name (e.g., 'Oct')
16 | day: "numeric", // numeric day of the month (e.g., '25')
17 | hour: "numeric", // numeric hour (e.g., '8')
18 | minute: "numeric", // numeric minute (e.g., '30')
19 | hour12: true, // use 12-hour clock (true) or 24-hour clock (false)
20 | };
21 |
22 | const dateDayOptions: Intl.DateTimeFormatOptions = {
23 | weekday: "short", // abbreviated weekday name (e.g., 'Mon')
24 | year: "numeric", // numeric year (e.g., '2023')
25 | month: "2-digit", // abbreviated month name (e.g., 'Oct')
26 | day: "2-digit", // numeric day of the month (e.g., '25')
27 | };
28 |
29 | const dateOptions: Intl.DateTimeFormatOptions = {
30 | month: "short", // abbreviated month name (e.g., 'Oct')
31 | year: "numeric", // numeric year (e.g., '2023')
32 | day: "numeric", // numeric day of the month (e.g., '25')
33 | };
34 |
35 | const timeOptions: Intl.DateTimeFormatOptions = {
36 | hour: "numeric", // numeric hour (e.g., '8')
37 | minute: "numeric", // numeric minute (e.g., '30')
38 | hour12: true, // use 12-hour clock (true) or 24-hour clock (false)
39 | };
40 |
41 | const formattedDateTime: string = new Date(dateString).toLocaleString(
42 | "en-US",
43 | dateTimeOptions
44 | );
45 |
46 | const formattedDateDay: string = new Date(dateString).toLocaleString(
47 | "en-US",
48 | dateDayOptions
49 | );
50 |
51 | const formattedDate: string = new Date(dateString).toLocaleString(
52 | "en-US",
53 | dateOptions
54 | );
55 |
56 | const formattedTime: string = new Date(dateString).toLocaleString(
57 | "en-US",
58 | timeOptions
59 | );
60 |
61 | return {
62 | dateTime: formattedDateTime,
63 | dateDay: formattedDateDay,
64 | dateOnly: formattedDate,
65 | timeOnly: formattedTime,
66 | };
67 | };
68 |
69 | export function formatAmount(amount: number): string {
70 | const formatter = new Intl.NumberFormat("en-IN", {
71 | style: "currency",
72 | currency: "INR",
73 | minimumFractionDigits: 2,
74 | });
75 |
76 | return formatter.format(amount);
77 | }
78 |
79 | export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
80 |
81 | export const removeSpecialCharacters = (value: string) => {
82 | return value.replace(/[^\w\s]/gi, "");
83 | };
84 |
85 | interface UrlQueryParams {
86 | params: string;
87 | key: string;
88 | value: string;
89 | }
90 |
91 | export function formUrlQuery({ params, key, value }: UrlQueryParams) {
92 | const currentUrl = qs.parse(params);
93 |
94 | currentUrl[key] = value;
95 |
96 | return qs.stringifyUrl(
97 | {
98 | url: window.location.pathname,
99 | query: currentUrl,
100 | },
101 | { skipNull: true }
102 | );
103 | }
104 |
105 | export function getAccountTypeColors(type: AccountTypes) {
106 | switch (type) {
107 | case "depository":
108 | return {
109 | bg: "bg-blue-25",
110 | lightBg: "bg-blue-100",
111 | title: "text-blue-900",
112 | subText: "text-blue-700",
113 | };
114 |
115 | case "credit":
116 | return {
117 | bg: "bg-success-25",
118 | lightBg: "bg-success-100",
119 | title: "text-success-900",
120 | subText: "text-success-700",
121 | };
122 |
123 | default:
124 | return {
125 | bg: "bg-green-25",
126 | lightBg: "bg-green-100",
127 | title: "text-green-900",
128 | subText: "text-green-700",
129 | };
130 | }
131 | }
132 |
133 | export function countTransactionCategories(
134 | transactions: Transaction[]
135 | ): CategoryCount[] {
136 | const categoryCounts: { [category: string]: number } = {};
137 | let totalCount = 0;
138 |
139 | // Iterate over each transaction
140 | transactions &&
141 | transactions.forEach((transaction) => {
142 | // Extract the category from the transaction
143 | const category = transaction.category;
144 |
145 | // If the category exists in the categoryCounts object, increment its count
146 | if (categoryCounts.hasOwnProperty(category)) {
147 | categoryCounts[category]++;
148 | } else {
149 | // Otherwise, initialize the count to 1
150 | categoryCounts[category] = 1;
151 | }
152 |
153 | // Increment total count
154 | totalCount++;
155 | });
156 |
157 | // Convert the categoryCounts object to an array of objects
158 | const aggregatedCategories: CategoryCount[] = Object.keys(categoryCounts).map(
159 | (category) => ({
160 | name: category,
161 | count: categoryCounts[category],
162 | totalCount,
163 | })
164 | );
165 |
166 | // Sort the aggregatedCategories array by count in descending order
167 | aggregatedCategories.sort((a, b) => b.count - a.count);
168 |
169 | return aggregatedCategories;
170 | }
171 |
172 | export function extractCustomerIdFromUrl(url: string) {
173 | // Split the URL string by '/'
174 | const parts = url.split("/");
175 |
176 | // Extract the last part, which represents the customer ID
177 | const customerId = parts[parts.length - 1];
178 |
179 | return customerId;
180 | }
181 |
182 | export function encryptId(id: string) {
183 | return btoa(id);
184 | }
185 |
186 | export function decryptId(id: string) {
187 | return atob(id);
188 | }
189 |
190 | export const getTransactionStatus = (date: Date) => {
191 | const today = new Date();
192 | const twoDaysAgo = new Date(today);
193 | twoDaysAgo.setDate(today.getDate() - 2);
194 |
195 | return date > twoDaysAgo ? "Processing" : "Success";
196 | };
197 | export const AuthFormSchema = (type :string)=>z.object({
198 | //sign-up
199 | firstName: type==='sign-in'? z.string().optional() : z.string().min(4).max(15),
200 | lastName: type==='sign-in'? z.string().optional() : z.string().max(8),
201 | address1: type==='sign-in'? z.string().optional() : z.string().max(50),
202 | city: type==='sign-in'? z.string().optional() : z.string().max(10),
203 | state: type==='sign-in'? z.string().optional() : z.string().min(2).max(20 ),
204 | postalCode: z.string().min(3).max(6),
205 | dateOfBirth: type==='sign-in'? z.string().optional() : z.string().min(8),
206 | ssn: type==='sign-in'? z.string().optional() : z.string().min(3),
207 | //sign-in
208 | email: z.string().email(),
209 | password : z.string().min(8),
210 | })
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | declare type SearchParamProps = {
4 | params: { [key: string]: string };
5 | searchParams: { [key: string]: string | string[] | undefined };
6 | };
7 |
8 | // ========================================
9 |
10 | declare type SignUpParams = {
11 | firstName: string;
12 | lastName: string;
13 | address1: string;
14 | city: string;
15 | state: string;
16 | postalCode: string;
17 | dateOfBirth: string;
18 | ssn: string;
19 | email: string;
20 | password: string;
21 | };
22 |
23 | declare type LoginUser = {
24 | email: string;
25 | password: string;
26 | };
27 |
28 | declare type User = {
29 | $id: string;
30 | email: string;
31 | userId: string;
32 | dwollaCustomerUrl: string;
33 | dwollaCustomerId: string;
34 | firstName: string;
35 | lastName: string;
36 | name:string;
37 | address1: string;
38 | city: string;
39 | state: string;
40 | postalCode: string;
41 | dateOfBirth: string;
42 | ssn: string;
43 | };
44 |
45 | declare type NewUserParams = {
46 | userId: string;
47 | email: string;
48 | name: string;
49 | password: string;
50 | };
51 |
52 | declare type Account = {
53 | id: string;
54 | availableBalance: number;
55 | currentBalance: number;
56 | officialName: string;
57 | mask: string;
58 | institutionId: string;
59 | name: string;
60 | type: string;
61 | subtype: string;
62 | appwriteItemId: string;
63 | sharableId: string;
64 | };
65 |
66 | declare type Transaction = {
67 | id: string;
68 | $id: string;
69 | name: string;
70 | paymentChannel: string;
71 | type: string;
72 | accountId: string;
73 | amount: number;
74 | pending: boolean;
75 | category: string;
76 | date: string;
77 | image: string;
78 | type: string;
79 | $createdAt: string;
80 | channel: string;
81 | senderBankId: string;
82 | receiverBankId: string;
83 | };
84 |
85 | declare type Bank = {
86 | $id: string;
87 | accountId: string;
88 | bankId: string;
89 | accessToken: string;
90 | fundingSourceUrl: string;
91 | userId: string;
92 | sharableId: string;
93 | };
94 |
95 | declare type AccountTypes =
96 | | "depository"
97 | | "credit"
98 | | "loan "
99 | | "investment"
100 | | "other";
101 |
102 | declare type Category = "Food and Drink" | "Travel" | "Transfer";
103 |
104 | declare type CategoryCount = {
105 | name: string;
106 | count: number;
107 | totalCount: number;
108 | };
109 |
110 | declare type Receiver = {
111 | firstName: string;
112 | lastName: string;
113 | };
114 |
115 | declare type TransferParams = {
116 | sourceFundingSourceUrl: string;
117 | destinationFundingSourceUrl: string;
118 | amount: string;
119 | };
120 |
121 | declare type AddFundingSourceParams = {
122 | dwollaCustomerId: string;
123 | processorToken: string;
124 | bankName: string;
125 | };
126 |
127 | declare type NewDwollaCustomerParams = {
128 | firstName: string;
129 | lastName: string;
130 | email: string;
131 | type: string;
132 | address1: string;
133 | city: string;
134 | state: string;
135 | postalCode: string;
136 | dateOfBirth: string;
137 | ssn: string;
138 |
139 | };
140 |
141 | declare interface CreditCardProps {
142 | account: Account;
143 | userName: string;
144 | showBalance?: boolean;
145 | }
146 |
147 | declare interface BankInfoProps {
148 | account: Account;
149 | appwriteItemId?: string;
150 | type: "full" | "card";
151 | }
152 |
153 | declare interface HeaderBoxProps {
154 | type?: "title" | "greeting";
155 | title: string;
156 | subtext: string;
157 | user?: string;
158 | }
159 |
160 | declare interface MobileNavProps {
161 | user: User;
162 | }
163 |
164 | declare interface PageHeaderProps {
165 | topTitle: string;
166 | bottomTitle: string;
167 | topDescription: string;
168 | bottomDescription: string;
169 | connectBank?: boolean;
170 | }
171 |
172 | declare interface PaginationProps {
173 | page: number;
174 | totalPages: number;
175 | }
176 |
177 | declare interface PlaidLinkProps {
178 | user: User;
179 | variant?: "primary" | "ghost";
180 | dwollaCustomerId?: string;
181 | }
182 |
183 | // declare type User = sdk.Models.Document & {
184 | // accountId: string;
185 | // email: string;
186 | // name: string;
187 | // items: string[];
188 | // accessToken: string;
189 | // image: string;
190 | // };
191 |
192 | declare interface AuthFormProps {
193 | type: "sign-in" | "sign-up";
194 | }
195 |
196 | declare interface BankDropdownProps {
197 | accounts: Account[];
198 | setValue?: UseFormSetValue;
199 | otherStyles?: string;
200 | }
201 |
202 | declare interface BankTabItemProps {
203 | account: Account;
204 | appwriteItemId?: string;
205 | }
206 |
207 | declare interface TotlaBalanceBoxProps {
208 | accounts: Account[];
209 | totalBanks: number;
210 | totalCurrentBalance: number;
211 | }
212 |
213 | declare interface FooterProps {
214 | user: User;
215 | type : "mobile" | "desktop"
216 | }
217 |
218 | declare interface RightSidebarProps {
219 | user: User;
220 | transactions: Transaction[];
221 | banks: Bank[] & Account[];
222 | }
223 |
224 | declare interface SiderbarProps {
225 | user: User;
226 | }
227 |
228 | declare interface RecentTransactionsProps {
229 | accounts: Account[];
230 | transactions: Transaction[];
231 | appwriteItemId: string;
232 | page: number;
233 | }
234 |
235 | declare interface TransactionHistoryTableProps {
236 | transactions: Transaction[];
237 | page: number;
238 | }
239 |
240 | declare interface CategoryBadgeProps {
241 | category: string;
242 | }
243 |
244 | declare interface TransactionTableProps {
245 | transactions: Transaction[];
246 | }
247 |
248 | declare interface CategoryProps {
249 | category: CategoryCount;
250 | }
251 |
252 | declare interface DoughnutChartProps {
253 | accounts: Account[];
254 | }
255 |
256 | declare interface PaymentTransferFormProps {
257 | accounts: Account[];
258 | }
259 |
260 | // Actions
261 | declare interface getAccountsProps {
262 | userId: string;
263 | }
264 |
265 | declare interface getAccountProps {
266 | appwriteItemId: string;
267 | }
268 |
269 | declare interface getInstitutionProps {
270 | institutionId: string;
271 | }
272 |
273 | declare interface getTransactionsProps {
274 | accessToken: string;
275 | }
276 |
277 | declare interface CreateFundingSourceOptions {
278 | customerId: string; // Dwolla Customer ID
279 | fundingSourceName: string; // Dwolla Funding Source Name
280 | plaidToken: string; // Plaid Account Processor Token
281 | _links: object; // Dwolla On Demand Authorization Link
282 | }
283 |
284 | declare interface CreateTransactionProps {
285 | name: string;
286 | amount: string;
287 | senderId: string;
288 | senderBankId: string;
289 | receiverId: string;
290 | receiverBankId: string;
291 | email: string;
292 | }
293 |
294 | declare interface getTransactionsByBankIdProps {
295 | bankId: string;
296 | }
297 |
298 | declare interface signInProps {
299 | email: string;
300 | password: string;
301 | }
302 |
303 | declare interface getUserInfoProps {
304 | userId: string;
305 | }
306 |
307 | declare interface exchangePublicTokenProps {
308 | publicToken: string;
309 | user: User;
310 | }
311 |
312 | declare interface createBankAccountProps {
313 | accessToken: string;
314 | userId: string;
315 | accountId: string;
316 | bankId: string;
317 | fundingSourceUrl: string;
318 | sharableId: string;
319 | }
320 |
321 | declare interface getBanksProps {
322 | userId: string;
323 | }
324 |
325 | declare interface getBankProps {
326 | documentId: string;
327 | }
328 |
329 | declare interface getBankByAccountIdProps {
330 | accountId: string;
331 | }
332 |
--------------------------------------------------------------------------------
/public/icons/stripe.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | /* Hide scrollbar for Chrome, Safari and Opera */
12 | .no-scrollbar::-webkit-scrollbar {
13 | display: none;
14 | }
15 |
16 | /* Hide scrollbar for IE, Edge and Firefox */
17 | .no-scrollbar {
18 | -ms-overflow-style: none; /* IE and Edge */
19 | scrollbar-width: none; /* Firefox */
20 | }
21 | .glassmorphism {
22 | background: rgba(255, 255, 255, 0.25);
23 | backdrop-filter: blur(4px);
24 | -webkit-backdrop-filter: blur(4px);
25 | }
26 |
27 | .custom-scrollbar::-webkit-scrollbar {
28 | width: 3px;
29 | height: 3px;
30 | border-radius: 2px;
31 | }
32 |
33 | .custom-scrollbar::-webkit-scrollbar-track {
34 | background: #dddddd;
35 | }
36 |
37 | .custom-scrollbar::-webkit-scrollbar-thumb {
38 | background: #5c5c7b;
39 | border-radius: 50px;
40 | }
41 |
42 | .custom-scrollbar::-webkit-scrollbar-thumb:hover {
43 | background: #7878a3;
44 | }
45 |
46 | @layer utilities {
47 | .input-class {
48 | @apply text-16 placeholder:text-16 rounded-lg border border-gray-300 text-gray-900 placeholder:text-gray-500;
49 | }
50 |
51 | .sheet-content button {
52 | @apply focus:ring-0 focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important;
53 | }
54 |
55 | .text14_padding10 {
56 | @apply text-14 px-4 py-2.5 font-semibold;
57 | }
58 |
59 | .flex-center {
60 | @apply flex items-center justify-center;
61 | }
62 |
63 | .header-2 {
64 | @apply text-18 font-semibold text-gray-900;
65 | }
66 |
67 | .text-10 {
68 | @apply text-[10px] leading-[14px];
69 | }
70 |
71 | .text-12 {
72 | @apply text-[12px] leading-[16px];
73 | }
74 |
75 | .text-14 {
76 | @apply text-[14px] leading-[20px];
77 | }
78 |
79 | .text-16 {
80 | @apply text-[16px] leading-[24px];
81 | }
82 |
83 | .text-18 {
84 | @apply text-[18px] leading-[22px];
85 | }
86 |
87 | .text-20 {
88 | @apply text-[20px] leading-[24px];
89 | }
90 |
91 | .text-24 {
92 | @apply text-[24px] leading-[30px];
93 | }
94 |
95 | .text-26 {
96 | @apply text-[26px] leading-[32px];
97 | }
98 |
99 | .text-30 {
100 | @apply text-[30px] leading-[38px];
101 | }
102 |
103 | .text-36 {
104 | @apply text-[36px] leading-[44px];
105 | }
106 |
107 | /* Home */
108 | .home {
109 | @apply no-scrollbar flex w-full flex-row max-xl:max-h-screen max-xl:overflow-y-scroll;
110 | }
111 |
112 | .home-content {
113 | @apply no-scrollbar flex w-full flex-1 flex-col gap-8 px-5 sm:px-8 py-7 lg:py-12 xl:max-h-screen xl:overflow-y-scroll;
114 | }
115 |
116 | .home-header {
117 | @apply flex flex-col justify-between gap-8;
118 | }
119 |
120 | .total-balance {
121 | @apply flex w-full items-center gap-4 rounded-xl border border-gray-200 p-4 shadow-chart sm:gap-6 sm:p-6;
122 | }
123 |
124 | .total-balance-chart {
125 | @apply flex size-full max-w-[100px] items-center sm:max-w-[120px];
126 | }
127 |
128 | .total-balance-label {
129 | @apply text-14 font-medium text-gray-600;
130 | }
131 |
132 | .total-balance-amount {
133 | @apply text-24 lg:text-30 flex-1 font-semibold text-gray-900;
134 | }
135 |
136 | .recent-transactions {
137 | @apply flex w-full flex-col gap-6;
138 | }
139 |
140 | .view-all-btn {
141 | @apply text-14 rounded-lg border border-gray-300 px-4 py-2.5 font-semibold text-gray-700;
142 | }
143 |
144 | .recent-transactions {
145 | @apply flex w-full flex-col gap-6;
146 | }
147 |
148 | .recent-transactions-label {
149 | @apply text-20 md:text-24 font-semibold text-gray-900;
150 | }
151 |
152 | .recent-transactions-tablist {
153 | @apply custom-scrollbar mb-8 flex w-full flex-nowrap;
154 | }
155 |
156 | /* Right sidebar */
157 | .right-sidebar {
158 | @apply no-scrollbar hidden h-screen max-h-screen flex-col border-l border-gray-200 xl:flex w-[355px] xl:overflow-y-scroll !important;
159 | }
160 |
161 | .profile-banner {
162 | @apply h-[120px] w-full bg-gradient-mesh bg-cover bg-no-repeat;
163 | }
164 |
165 | .profile {
166 | @apply relative flex px-6 max-xl:justify-center;
167 | }
168 |
169 | .profile-img {
170 | @apply flex-center absolute -top-8 size-24 rounded-full bg-gray-100 border-8 border-white p-2 shadow-profile;
171 | }
172 |
173 | .profile-details {
174 | @apply flex flex-col pt-24;
175 | }
176 |
177 | .profile-name {
178 | @apply text-24 font-semibold text-gray-900;
179 | }
180 |
181 | .profile-email {
182 | @apply text-16 font-normal text-gray-600;
183 | }
184 |
185 | .banks {
186 | @apply flex flex-col justify-between gap-8 px-6 py-8;
187 | }
188 |
189 | /* My Banks */
190 | .my-banks {
191 | @apply flex h-screen max-h-screen w-full flex-col gap-8 bg-gray-25 p-8 xl:py-12;
192 | }
193 |
194 | /* My Banks */
195 | .transactions {
196 | @apply flex max-h-screen w-full flex-col gap-8 overflow-y-scroll bg-gray-25 p-8 xl:py-12;
197 | }
198 |
199 | .transactions-header {
200 | @apply flex w-full flex-col items-start justify-between gap-8 md:flex-row;
201 | }
202 |
203 | .transactions-account {
204 | @apply flex flex-col justify-between gap-4 rounded-lg border-y bg-blue-600 px-4 py-5 md:flex-row;
205 | }
206 |
207 | .transactions-account-balance {
208 | @apply flex-center flex-col gap-2 rounded-md bg-blue-25/20 px-4 py-2 text-white;
209 | }
210 |
211 | .header-box {
212 | @apply flex flex-col gap-1;
213 | }
214 |
215 | .header-box-title {
216 | @apply text-24 lg:text-30 font-semibold text-gray-900;
217 | }
218 |
219 | .header-box-subtext {
220 | @apply text-14 lg:text-16 font-normal text-gray-600;
221 | }
222 |
223 | /* Bank Card */
224 | .bank-card {
225 | @apply relative flex h-[190px] w-full max-w-[320px] justify-between rounded-[20px] border border-white bg-bank-gradient shadow-creditCard backdrop-blur-[6px];
226 | }
227 |
228 | .bank-card_content {
229 | @apply relative z-10 flex size-full max-w-[228px] flex-col justify-between rounded-l-[20px] bg-gray-700 bg-bank-gradient px-5 pb-4 pt-5;
230 | }
231 |
232 | .bank-card_icon {
233 | @apply flex size-full flex-1 flex-col items-end justify-between rounded-r-[20px] bg-bank-gradient bg-cover bg-center bg-no-repeat py-5 pr-5;
234 | }
235 |
236 | /* Bank Info */
237 | .bank-info {
238 | @apply gap-[18px] flex p-4 transition-all border bg-blue-25 border-transparent;
239 | }
240 |
241 | /* Category Badge */
242 | .category-badge {
243 | @apply flex-center truncate w-fit gap-1 rounded-2xl border-[1.5px] py-[2px] pl-1.5 pr-2;
244 | }
245 |
246 | .banktab-item {
247 | @apply gap-[18px] border-b-2 flex px-2 sm:px-4 py-2 transition-all;
248 | }
249 |
250 | /* Mobile nav */
251 | .mobilenav-sheet {
252 | @apply flex h-[calc(100vh-72px)] flex-col justify-between overflow-y-auto;
253 | }
254 |
255 | .mobilenav-sheet_close {
256 | @apply flex gap-3 items-center p-4 rounded-lg w-full max-w-60;
257 | }
258 |
259 | /* PlaidLink */
260 | .plaidlink-primary {
261 | @apply text-16 rounded-lg border border-bankGradient bg-bank-gradient font-semibold text-white shadow-form;
262 | }
263 |
264 | .plaidlink-ghost {
265 | @apply flex cursor-pointer items-center justify-center gap-3 rounded-lg px-3 py-7 hover:bg-white lg:justify-start;
266 | }
267 |
268 | .plaidlink-default {
269 | @apply flex !justify-start cursor-pointer gap-3 rounded-lg !bg-transparent flex-row;
270 | }
271 |
272 | /* Auth */
273 | .auth-asset {
274 | @apply flex h-screen w-full sticky top-0 items-center justify-end bg-sky-1 max-lg:hidden;
275 | }
276 |
277 | /* Auth Form */
278 | .auth-form {
279 | @apply flex min-h-screen w-full max-w-[420px] flex-col justify-center gap-5 py-10 md:gap-8;
280 | }
281 |
282 | .form-item {
283 | @apply flex flex-col gap-1.5;
284 | }
285 |
286 | .form-label {
287 | @apply text-14 w-full max-w-[280px] font-medium text-gray-700;
288 | }
289 |
290 | .form-message {
291 | @apply text-12 text-red-500;
292 | }
293 |
294 | .form-btn {
295 | @apply text-16 rounded-lg border border-bankGradient bg-bank-gradient font-semibold text-white shadow-form;
296 | }
297 |
298 | .form-link {
299 | @apply text-14 cursor-pointer font-medium text-bankGradient;
300 | }
301 |
302 | /* Payment Transfer */
303 | .payment-transfer {
304 | @apply no-scrollbar flex flex-col overflow-y-scroll bg-gray-25 p-8 md:max-h-screen xl:py-12;
305 | }
306 |
307 | .payment-transfer_form-item {
308 | @apply flex w-full max-w-[850px] flex-col gap-3 md:flex-row lg:gap-8;
309 | }
310 |
311 | .payment-transfer_form-content {
312 | @apply flex w-full max-w-[280px] flex-col gap-2;
313 | }
314 |
315 | .payment-transfer_form-details {
316 | @apply flex flex-col gap-1 border-t border-gray-200 pb-5 pt-6;
317 | }
318 |
319 | .payment-transfer_btn-box {
320 | @apply mt-5 flex w-full max-w-[850px] gap-3 border-gray-200 py-5;
321 | }
322 |
323 | .payment-transfer_btn {
324 | @apply text-14 w-full bg-bank-gradient font-semibold text-white shadow-form !important;
325 | }
326 |
327 | /* Root Layout */
328 | .root-layout {
329 | @apply flex h-16 items-center justify-between p-5 shadow-creditCard sm:p-8 md:hidden;
330 | }
331 |
332 | /* Bank Info */
333 | .bank-info_content {
334 | @apply flex flex-1 items-center justify-between gap-2 overflow-hidden;
335 | }
336 |
337 | /* Footer */
338 | .footer {
339 | @apply flex cursor-pointer items-center justify-between gap-2 py-6 ;
340 | }
341 |
342 | .footer_name {
343 | @apply flex size-10 items-center justify-center rounded-full bg-gray-200 max-xl:hidden;
344 | }
345 |
346 | .footer_email {
347 | @apply flex flex-1 flex-col justify-center max-xl:hidden;
348 | }
349 |
350 | .footer_name-mobile {
351 | @apply flex size-10 items-center justify-center rounded-full bg-gray-200;
352 | }
353 |
354 | .footer_email-mobile {
355 | @apply flex flex-1 flex-col justify-center;
356 | }
357 |
358 | .footer_image {
359 | @apply relative size-5 max-xl:w-full max-xl:flex max-xl:justify-center max-xl:items-center;
360 | }
361 |
362 | .footer_image-mobile {
363 | @apply relative size-5;
364 | }
365 |
366 | /* Sidebar */
367 | .sidebar {
368 | @apply sticky left-0 top-0 flex h-screen w-fit flex-col justify-between border-r border-gray-200 bg-white pt-8 text-white max-md:hidden sm:p-4 xl:p-6 2xl:w-[355px];
369 | }
370 |
371 | .sidebar-logo {
372 | @apply 2xl:text-26 font-ibm-plex-serif text-[26px] font-bold text-black-1 max-xl:hidden;
373 | }
374 |
375 | .sidebar-link {
376 | @apply flex gap-3 items-center py-1 md:p-3 2xl:p-4 rounded-lg justify-center xl:justify-start;
377 | }
378 |
379 | .sidebar-label {
380 | @apply text-16 font-semibold text-black-2 max-xl:hidden;
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/public/icons/spotify.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/icons/figma.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/icons/jsm.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------