├── .eslintrc.json ├── bun.lockb ├── app ├── favicon.ico ├── (dashboard) │ ├── page.tsx │ ├── layout.tsx │ ├── transactions │ │ ├── account-column.tsx │ │ ├── upload-button.tsx │ │ ├── category-column.tsx │ │ ├── import-table.tsx │ │ ├── table-head-select.tsx │ │ ├── actions.tsx │ │ ├── columns.tsx │ │ ├── import-card.tsx │ │ └── page.tsx │ ├── accounts │ │ ├── columns.tsx │ │ ├── actions.tsx │ │ └── page.tsx │ └── categories │ │ ├── columns.tsx │ │ ├── actions.tsx │ │ └── page.tsx ├── api │ └── [[...route]] │ │ ├── route.ts │ │ ├── accounts.ts │ │ └── categories.ts ├── layout.tsx ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx └── globals.css ├── components ├── count-up.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── sonner.tsx │ ├── checkbox.tsx │ ├── tooltip.tsx │ ├── badge.tsx │ ├── popover.tsx │ ├── button.tsx │ ├── card.tsx │ ├── calendar.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── sheet.tsx │ └── form.tsx ├── filters.tsx ├── header-logo.tsx ├── welcome-msg.tsx ├── nav-button.tsx ├── radar-variant.tsx ├── data-charts.tsx ├── header.tsx ├── category-tooltip.tsx ├── bar-variant.tsx ├── line-variant.tsx ├── select.tsx ├── date-picker.tsx ├── custom-tooltip.tsx ├── data-grid.tsx ├── area-variant.tsx ├── account-filter.tsx ├── radial-variant.tsx ├── pie-variant.tsx ├── navigation.tsx ├── amount-input.tsx ├── date-filter.tsx ├── data-card.tsx ├── spending-pie.tsx └── chart.tsx ├── next.config.mjs ├── postcss.config.mjs ├── lib ├── hono.ts └── utils.ts ├── db ├── drizzle.ts └── schema.ts ├── .env.example ├── drizzle ├── meta │ ├── _journal.json │ └── 0000_snapshot.json └── 0000_odd_strong_guy.sql ├── drizzle.config.ts ├── features ├── accounts │ ├── hooks │ │ ├── use-new-account.ts │ │ ├── use-open-account.ts │ │ └── use-select-account.tsx │ ├── api │ │ ├── use-get-accounts.ts │ │ ├── use-get-account.ts │ │ ├── use-create-account.ts │ │ ├── use-bulk-delete-accounts.ts │ │ ├── use-delete-account.ts │ │ └── use-edit-account.ts │ └── components │ │ ├── new-account-sheet.tsx │ │ ├── account-form.tsx │ │ └── edit-account-sheet.tsx ├── categories │ ├── hooks │ │ ├── use-new-category.ts │ │ └── use-open-category.ts │ ├── api │ │ ├── use-get-categories.ts │ │ ├── use-get-category.ts │ │ ├── use-create-category.ts │ │ ├── use-bulk-delete-categories.ts │ │ ├── use-delete-category.ts │ │ └── use-edit-category.ts │ └── components │ │ ├── new-category-sheet.tsx │ │ ├── category-form.tsx │ │ └── edit-category-sheet.tsx ├── transactions │ ├── hooks │ │ ├── use-new-transaction.ts │ │ └── use-open-transaction.ts │ ├── api │ │ ├── use-get-transaction.ts │ │ ├── use-create-transaction.ts │ │ ├── use-get-transactions.ts │ │ ├── use-delete-transaction.ts │ │ ├── use-bulk-create-transactions.ts │ │ ├── use-bulk-delete-transactions.ts │ │ └── use-edit-transaction.ts │ └── components │ │ ├── new-transaction-sheet.tsx │ │ └── edit-transaction-sheet.tsx └── summary │ └── api │ └── use-get-summary.ts ├── components.json ├── middleware.ts ├── .gitignore ├── scripts ├── migrate.ts └── seed.ts ├── public ├── vercel.svg ├── next.svg └── logo.svg ├── tsconfig.json ├── providers ├── sheet-provider.tsx └── query-provider.tsx ├── README.md ├── hooks └── use-confirm.tsx ├── package.json └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davronov-Alimardon/finance-platform/HEAD/bun.lockb -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davronov-Alimardon/finance-platform/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /components/count-up.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import CountUp from "react-countup"; 4 | export { CountUp }; 5 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/hono.ts: -------------------------------------------------------------------------------- 1 | import { hc } from "hono/client"; 2 | 3 | import { AppType } from "@/app/api/[[...route]]/route"; 4 | 5 | export const client = hc(process.env.NEXT_PUBLIC_APP_URL!); 6 | -------------------------------------------------------------------------------- /db/drizzle.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import { drizzle } from "drizzle-orm/neon-http"; 3 | 4 | import * as schema from "./schema"; 5 | 6 | export const sql = neon(process.env.DATABASE_URL!); 7 | export const db = drizzle(sql, { schema }); 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_PUBLISHABLE_KEY= 3 | CLERK_SECRET_KEY= 4 | 5 | NEXT_PUBLIC_CLERK_SIGN_IN_URL= 6 | NEXT_PUBLIC_CLERK_SIGN_UP_URL= 7 | 8 | NEXT_PUBLIC_APP_URL=http://localhost:300 9 | 10 | DATABASE_URL="NEON DB POSTGRES URL" 11 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1720086676804, 9 | "tag": "0000_odd_strong_guy", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /app/(dashboard)/page.tsx: -------------------------------------------------------------------------------- 1 | import { DataGrid } from "@/components/data-grid"; 2 | import { DataCharts } from "@/components/data-charts"; 3 | 4 | export default function DashboardPage() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env.local" }); 5 | 6 | export default defineConfig({ 7 | schema: "./db/schema.ts", 8 | driver: "pg", 9 | dbCredentials: { 10 | connectionString: process.env.DATABASE_URL!, 11 | }, 12 | verbose: true, 13 | strict: true, 14 | }); 15 | -------------------------------------------------------------------------------- /components/filters.tsx: -------------------------------------------------------------------------------- 1 | import { DateFilter } from "@/components/date-filter"; 2 | import { AccountFilter } from "@/components/account-filter"; 3 | 4 | export const Filters = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /features/accounts/hooks/use-new-account.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type NewAccountState = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useNewAccount = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /features/categories/hooks/use-new-category.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type NewCategoryState = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useNewCategory = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/header"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | const DashboardLayout = ({ children }: Props) => { 8 | return ( 9 | <> 10 |
11 |
12 | {children} 13 |
14 | 15 | ); 16 | }; 17 | 18 | export default DashboardLayout; 19 | -------------------------------------------------------------------------------- /features/transactions/hooks/use-new-transaction.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type NewTransactionState = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useNewTransaction = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/header-logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | 4 | export const HeaderLogo = () => { 5 | return ( 6 | 7 |
8 | Logo 9 |

10 | Finance 11 |

12 |
13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /features/accounts/hooks/use-open-account.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type OpenAccountState = { 4 | id?: string; 5 | isOpen: boolean; 6 | onOpen: (id: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useOpenAccount = create((set) => ({ 11 | id: undefined, 12 | isOpen: false, 13 | onOpen: (id: string) => set({ isOpen: true, id }), 14 | onClose: () => set({ isOpen: false, id: undefined }), 15 | })); 16 | -------------------------------------------------------------------------------- /features/categories/hooks/use-open-category.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type OpenCategoryState = { 4 | id?: string; 5 | isOpen: boolean; 6 | onOpen: (id: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useOpenCategory = create((set) => ({ 11 | id: undefined, 12 | isOpen: false, 13 | onOpen: (id: string) => set({ isOpen: true, id }), 14 | onClose: () => set({ isOpen: false, id: undefined }), 15 | })); 16 | -------------------------------------------------------------------------------- /features/transactions/hooks/use-open-transaction.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type OpenTransactionState = { 4 | id?: string; 5 | isOpen: boolean; 6 | onOpen: (id: string) => void; 7 | onClose: () => void; 8 | }; 9 | 10 | export const useOpenTransaction = create((set) => ({ 11 | id: undefined, 12 | isOpen: false, 13 | onOpen: (id: string) => set({ isOpen: true, id }), 14 | onClose: () => set({ isOpen: false, id: undefined }), 15 | })); 16 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; 3 | 4 | const isProtectedRoute = createRouteMatcher([ 5 | "/", 6 | ]); 7 | 8 | export default clerkMiddleware((auth, request) => { 9 | if (isProtectedRoute(request)) { 10 | auth().protect(); 11 | } 12 | 13 | return NextResponse.next(); 14 | }); 15 | 16 | export const config = { 17 | matcher: ["/((?!.+.[w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 18 | }; 19 | -------------------------------------------------------------------------------- /components/welcome-msg.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/nextjs"; 4 | 5 | export const WelcomeMsg = () => { 6 | const { user, isLoaded } = useUser(); 7 | 8 | return ( 9 |
10 |

11 | Welcome Back{isLoaded ? ", " : " "}{user?.firstName} 👋🏻 12 |

13 |

14 | This is your Financial Overview Report 15 |

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /features/accounts/api/use-get-accounts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { client } from "@/lib/hono"; 4 | 5 | export const useGetAccounts = () => { 6 | const query = useQuery({ 7 | queryKey: ["accounts"], 8 | queryFn: async () => { 9 | const response = await client.api.accounts.$get(); 10 | 11 | if (!response.ok) { 12 | throw new Error("Failed to fetch accounts"); 13 | } 14 | 15 | const { data } = await response.json(); 16 | return data; 17 | }, 18 | }); 19 | 20 | return query; 21 | }; 22 | -------------------------------------------------------------------------------- /features/categories/api/use-get-categories.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { client } from "@/lib/hono"; 4 | 5 | export const useGetCategories = () => { 6 | const query = useQuery({ 7 | queryKey: ["categories"], 8 | queryFn: async () => { 9 | const response = await client.api.categories.$get(); 10 | 11 | if (!response.ok) { 12 | throw new Error("Failed to fetch categories"); 13 | } 14 | 15 | const { data } = await response.json(); 16 | return data; 17 | }, 18 | }); 19 | 20 | return query; 21 | }; 22 | -------------------------------------------------------------------------------- /scripts/migrate.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { neon } from "@neondatabase/serverless"; 3 | import { drizzle } from "drizzle-orm/neon-http"; 4 | import { migrate } from "drizzle-orm/neon-http/migrator"; 5 | 6 | config({ path: ".env.local" }); 7 | 8 | const sql = neon(process.env.DATABASE_URL!); 9 | const db = drizzle(sql); 10 | 11 | const main = async () => { 12 | try { 13 | await migrate(db, { migrationsFolder: "drizzle" }); 14 | } catch (error) { 15 | console.error("Error during migration:", error); 16 | process.exit(1); 17 | } 18 | }; 19 | 20 | main(); 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/(dashboard)/transactions/account-column.tsx: -------------------------------------------------------------------------------- 1 | import { useOpenAccount } from "@/features/accounts/hooks/use-open-account"; 2 | 3 | type Props = { 4 | account: string; 5 | accountId: string; 6 | }; 7 | 8 | export const AccountColumn = ({ 9 | account, 10 | accountId 11 | }: Props) => { 12 | const { onOpen: onOpenAccount } = useOpenAccount(); 13 | 14 | const onClick = () => { 15 | onOpenAccount(accountId); 16 | }; 17 | 18 | return ( 19 |
23 | {account} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /features/accounts/api/use-get-account.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { client } from "@/lib/hono"; 4 | 5 | export const useGetAccount = (id?: string) => { 6 | const query = useQuery({ 7 | enabled: !!id, 8 | queryKey: ["account", { id }], 9 | queryFn: async () => { 10 | const response = await client.api.accounts[":id"].$get({ 11 | param: { id }, 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error("Failed to fetch account"); 16 | } 17 | 18 | const { data } = await response.json(); 19 | return data; 20 | }, 21 | }); 22 | 23 | return query; 24 | }; 25 | -------------------------------------------------------------------------------- /features/categories/api/use-get-category.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { client } from "@/lib/hono"; 4 | 5 | export const useGetCategory = (id?: string) => { 6 | const query = useQuery({ 7 | enabled: !!id, 8 | queryKey: ["category", { id }], 9 | queryFn: async () => { 10 | const response = await client.api.categories[":id"].$get({ 11 | param: { id }, 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error("Failed to fetch category"); 16 | } 17 | 18 | const { data } = await response.json(); 19 | return data; 20 | }, 21 | }); 22 | 23 | return query; 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono" 2 | import { handle } from "hono/vercel"; 3 | 4 | import summary from "./summary"; 5 | import accounts from "./accounts"; 6 | import categories from "./categories"; 7 | import transactions from "./transactions"; 8 | 9 | export const runtime = "edge"; 10 | 11 | const app = new Hono().basePath("/api"); 12 | 13 | const routes = app 14 | .route("/summary", summary) 15 | .route("/accounts", accounts) 16 | .route("/categories", categories) 17 | .route("/transactions", transactions) 18 | 19 | export const GET = handle(app); 20 | export const POST = handle(app); 21 | export const PATCH = handle(app); 22 | export const DELETE = handle(app); 23 | 24 | export type AppType = typeof routes; 25 | -------------------------------------------------------------------------------- /app/(dashboard)/transactions/upload-button.tsx: -------------------------------------------------------------------------------- 1 | import { Upload } from "lucide-react"; 2 | import { useCSVReader } from "react-papaparse"; 3 | 4 | import { Button } from "@/components/ui/button"; 5 | 6 | type Props = { 7 | onUpload: (results: any) => void; 8 | }; 9 | 10 | export const UploadButton = ({ onUpload }: Props) => { 11 | const { CSVReader } = useCSVReader(); 12 | 13 | // TODO: Add a paywall 14 | 15 | return ( 16 | 17 | {({ getRootProps }: any) => ( 18 | 26 | )} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /features/transactions/api/use-get-transaction.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { client } from "@/lib/hono"; 4 | import { convertAmountFromMiliunits } from "@/lib/utils"; 5 | 6 | export const useGetTransaction = (id?: string) => { 7 | const query = useQuery({ 8 | enabled: !!id, 9 | queryKey: ["transaction", { id }], 10 | queryFn: async () => { 11 | const response = await client.api.transactions[":id"].$get({ 12 | param: { id }, 13 | }); 14 | 15 | if (!response.ok) { 16 | throw new Error("Failed to fetch transaction"); 17 | } 18 | 19 | const { data } = await response.json(); 20 | return { 21 | ...data, 22 | amount: convertAmountFromMiliunits(data.amount), 23 | }; 24 | }, 25 | }); 26 | 27 | return query; 28 | }; 29 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/nav-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | type Props = { 7 | href: string; 8 | label: string; 9 | isActive?: boolean; 10 | }; 11 | 12 | export const NavButton = ({ 13 | href, 14 | label, 15 | isActive, 16 | }: Props) => { 17 | return ( 18 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |