diff --git a/app/(dashboard)/dashboard/categories/page.tsx b/app/(dashboard)/dashboard/categories/page.tsx new file mode 100644 index 00000000..6ad948a9 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/page.tsx @@ -0,0 +1,58 @@ +import { cookies } from "next/headers"; +import Image from "next/image"; +import { redirect } from "next/navigation"; + +import { authOptions } from "@/lib/auth"; +import { getCurrentUser } from "@/lib/session"; +import { isValidJSONString } from "@/lib/utils"; +import { CategoriesDashboard } from "@/components/categories/components/categories-dashboard"; +import { accounts, mails } from "@/components/investments/data"; + +export const metadata = { + title: "Transactions", + description: "Transactions description", +}; + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user) { + redirect(authOptions?.pages?.signIn || "/login"); + } + + const layout = cookies().get("react-resizable-panels:layout"); + const collapsed = cookies().get("react-resizable-panels:collapsed"); + + const defaultLayout = layout ? JSON.parse(layout.value) : undefined; + const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined; + + return ( + <> +
+ Mail + Mail +
+
+ +
+ + ); +} diff --git a/components/categories/components/account-switcher.tsx b/components/categories/components/account-switcher.tsx new file mode 100644 index 00000000..142caf08 --- /dev/null +++ b/components/categories/components/account-switcher.tsx @@ -0,0 +1,63 @@ +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface AccountSwitcherProps { + isCollapsed: boolean; + accounts: { + label: string; + email: string; + icon: React.ReactNode; + }[]; +} + +export function AccountSwitcher({ + isCollapsed, + accounts, +}: AccountSwitcherProps) { + const [selectedAccount, setSelectedAccount] = React.useState( + accounts[0].email, + ); + + return ( + + ); +} diff --git a/components/categories/components/accounts-list.tsx b/components/categories/components/accounts-list.tsx new file mode 100644 index 00000000..9369520b --- /dev/null +++ b/components/categories/components/accounts-list.tsx @@ -0,0 +1,73 @@ +import React, { ComponentProps } from "react"; +import formatDistanceToNow from "date-fns/formatDistanceToNow"; +import { ChevronRight } from "lucide-react"; + +import { formatCurrency } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +import { Mail } from "../data"; +import { useMail } from "../use-mail"; + +interface MailListProps { + items: Mail[]; +} + +export function AccountsList({ items }: MailListProps) { + const [mail, setMail] = useMail(); + + // Group items by category + const groupedItems = items.reduce( + (acc, item) => { + if (!acc[item.category]) { + acc[item.category] = []; + } + acc[item.category].push(item); + return acc; + }, + {} as Record, + ); + + return ( + +
+ {Object.entries(groupedItems).map(([category, items]) => ( + +

{category}

+ {items.map((item) => ( + setMail({ ...mail, selected: item.id })} + className="group flex items-center justify-between p-3 hover:bg-gray-100" + > +
+ {item.name} +

+ {formatDistanceToNow(new Date(item.date), { + addSuffix: true, + })} +

+
+
+ = 0 ? "default" : "destructive"} + className="self-start" + > + {item.change >= 0 + ? `▲ ${item.change}%` + : `▼ ${Math.abs(item.change)}%`} + +

+ {formatCurrency(item.available)} +

+ +
+
+ ))} +
+ ))} +
+
+ ); +} diff --git a/components/categories/components/accounts-review-table.tsx b/components/categories/components/accounts-review-table.tsx new file mode 100644 index 00000000..099a143b --- /dev/null +++ b/components/categories/components/accounts-review-table.tsx @@ -0,0 +1,350 @@ +"use client"; + +import * as React from "react"; +import { + CaretSortIcon, + ChevronDownIcon, + DotsHorizontalIcon, +} from "@radix-ui/react-icons"; +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; +import { compareAsc, format, parseISO } from "date-fns"; +import { + ArrowRightLeftIcon, + BadgeDollarSign, + Building, + Car, + Check, + CreditCard, + Divide, + Mail, + MessageSquare, + PlusCircle, + Repeat2, + Settings, + ShoppingCartIcon, + UserPlus, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { payments } from "../data"; + +export type Payment = { + id: string; + amount: number; + status: string; + label: string; + date: string; +}; + +// Define the icon mapping +const labelToIconMap = { + Subscriptions: , + Car: , // Use the actual icon component + House: , + Food: , + // Add other mappings as needed +}; + +// Helper function to get the icon based on the label +const getIconForLabel = (label) => { + return labelToIconMap[label] || null; // Return null if no icon is found for the label +}; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "status", + header: "Description", + cell: ({ row }) => ( +
{row.getValue("status")}
+ ), + }, + { + accessorKey: "label", + header: "Label", + cell: ({ row }) => { + const label = row.getValue("label") as string; + const icon = getIconForLabel(label); + let badgeVariant; + + // Example logic to determine the badge variant based on the label + switch (label) { + case "Subscriptions": + badgeVariant = "default"; + break; + case "Car": + badgeVariant = "secondary"; + break; + case "House": + badgeVariant = "destructive"; + break; + case "Food": + default: + badgeVariant = "outline"; + break; + } + + return ( + + {icon && React.cloneElement(icon, { className: "h-4 w-4" })} + {label} + + ); + }, + }, + { + accessorKey: "amount", + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")); + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + + return
{formatted}
; + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + + return ( + + + + + + Actions + navigator.clipboard.writeText(payment.id)} + > + + Review + + + + + Split + + + + Recurring + + + + + + Transaction Type + + + + + + Internal transfer + + + + Regular + + + + + More... + + + + + + + ); + }, + }, +]; + +export function AccountsReviewTable({ mailId }) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const filteredPayments = React.useMemo(() => { + return payments.filter((payment) => payment.mailId === mailId); + }, [mailId]); + + const table = useReactTable({ + data: filteredPayments, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + const groupedData = React.useMemo(() => { + // First sort the items by date + const sortedItems = filteredPayments.sort((a, b) => + compareAsc(parseISO(b.date), parseISO(a.date)), + ); + + // Then group items by month and year + return sortedItems.reduce( + (acc, item) => { + const monthYear = format(parseISO(item.date), "MMMM yyyy"); + if (!acc[monthYear]) { + acc[monthYear] = []; + } + acc[monthYear].push(item); + return acc; + }, + {} as Record, + ); + }, [filteredPayments]); + + return ( +
+ {Object.entries(groupedData).map(([monthYear, payments]) => ( + +

{monthYear}

+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ))} +
+ ); +} diff --git a/components/categories/components/allocation-section.tsx b/components/categories/components/allocation-section.tsx new file mode 100644 index 00000000..68bb6d30 --- /dev/null +++ b/components/categories/components/allocation-section.tsx @@ -0,0 +1,73 @@ +import React, { ComponentProps } from "react"; +import formatDistanceToNow from "date-fns/formatDistanceToNow"; +import { ChevronRight } from "lucide-react"; + +import { formatCurrency } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +import { Mail } from "../data"; +import { useMail } from "../use-mail"; + +interface MailListProps { + items: Mail[]; +} + +export function AllocationSection({ items }: MailListProps) { + const [mail, setMail] = useMail(); + + // Group items by category + const groupedItems = items.reduce( + (acc, item) => { + if (!acc[item.category]) { + acc[item.category] = []; + } + acc[item.category].push(item); + return acc; + }, + {} as Record, + ); + + return ( + +
+ {Object.entries(groupedItems).map(([category, items]) => ( + +

asdasdadsd{category}

+ {items.map((item) => ( + setMail({ ...mail, selected: item.id })} + className="group flex items-center justify-between p-3 hover:bg-gray-100" + > +
+ {item.name} +

+ {formatDistanceToNow(new Date(item.date), { + addSuffix: true, + })} +

+
+
+ = 0 ? "default" : "destructive"} + className="self-start" + > + {item.change >= 0 + ? `▲ ${item.change}%` + : `▼ ${Math.abs(item.change)}%`} + +

+ {formatCurrency(item.available)} +

+ +
+
+ ))} +
+ ))} +
+
+ ); +} diff --git a/components/categories/components/allocation-table.tsx b/components/categories/components/allocation-table.tsx new file mode 100644 index 00000000..46d53800 --- /dev/null +++ b/components/categories/components/allocation-table.tsx @@ -0,0 +1,275 @@ +"use client"; + +import * as React from "react"; +import { BoltIcon } from "@heroicons/react/20/solid"; +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; +import { + ArrowRightLeftIcon, + BadgeDollarSign, + Building, + Car, + CarIcon, + Check, + CoffeeIcon, + CreditCard, + Divide, + HomeIcon, + KeyIcon, + Mail, + MessageSquare, + PlusCircle, + Repeat2, + Settings, + ShoppingBagIcon, + ShoppingCartIcon, + StarIcon, + UserPlus, +} from "lucide-react"; + +import { Progress } from "@/components/ui/progress"; +import { ProgressCategories } from "@/components/ui/progress-categoires"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const data: Category[] = [ + { + id: "home", + name: "Home", + icon: "Home", + current: 4000, // Budget + spent: 3000, // Under Budget + totalpercentage: 2, + }, + { + id: "car", + name: "Car & Transportation", + icon: "Car", + current: 2000, // Budget + spent: 5000, // Over Budget + totalpercentage: 2, + }, + { + id: "food", + name: "Food & Drinks", + icon: "Coffee", + current: 1500, // Budget + spent: 1400, // Near Budget + totalpercentage: 2, + }, + { + id: "shopping", + name: "Shopping", + icon: "ShoppingBag", + current: 800, // Budget + spent: 500, // Under Budget + totalpercentage: 2, + }, + { + id: "entertainment", + name: "Entertainment", + icon: "Star", + current: 1200, // Budget + spent: 1300, // Over Budget + totalpercentage: 2, + }, + { + id: "subscriptions", + name: "Subscriptions", + icon: "CreditCard", + current: 400, // Budget + spent: 380, // Near Budget + totalpercentage: 2, + }, + { + id: "other", + name: "Other", + icon: "Settings", + current: 1000, // Budget + spent: 950, // Near Budget + totalpercentage: 2, + }, +]; + +export type Category = { + id: string; + name: string; + icon: string; + current: number; + spent: number; + totalpercentage: number; +}; + +const iconMap = { + Home: , + Car: , + Key: , + Bolt: , + Star: , + ShoppingBag: , + Coffee: , + CreditCard: , + Settings: , + // ... you can add other icons as needed +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Category", + cell: ({ row }) => { + const name = row.getValue("name") as string; + const icon = row.original.icon; // Assuming 'icon' is the key in your data for icon names + return ( +
+ {iconMap[icon]} {/* Render the corresponding icon */} +
{name}
+
+ ); + }, + }, + { + accessorKey: "spent", + header: () =>
Spent
, + cell: ({ row }) => { + const spent = parseFloat(row.getValue("spent")); + + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(spent); + + return
{formatted}
; + }, + }, + { + id: "progress", + header: "Portfolio Spread", + cell: ({ row }) => { + const current = row.getValue("current") as number; + const spent = row.getValue("spent") as number; + + const progressPercent = current > 0 ? (spent / current) * 100 : 0; + + return ( +
+ +
+ ); + }, + }, + + { + accessorKey: "current", + header: () =>
Budget
, + cell: ({ row }) => { + const budget = parseFloat(row.getValue("current")); + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(budget); + + return
{formatted}
; + }, + }, +]; + +export function CategoriesTable() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/components/categories/components/categories-dashboard.tsx b/components/categories/components/categories-dashboard.tsx new file mode 100644 index 00000000..394a3e99 --- /dev/null +++ b/components/categories/components/categories-dashboard.tsx @@ -0,0 +1,255 @@ +"use client"; + +import * as React from "react"; +import { + BadgeDollarSign, + BarChart, + Briefcase, + Building, + CreditCard, + DollarSign, + HelpCircle, + Layers, + LayoutDashboard, + PiggyBank, + Repeat2, + Settings, + Tag, +} from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { TopCategoriesTable } from "@/components/new-dashboard/components/top-categories-table"; + +import { Mail } from "../data"; +import { useMail } from "../use-mail"; +import { AccountSwitcher } from "./account-switcher"; +import { AccountsList } from "./accounts-list"; +import { CategoriesTable } from "./allocation-table"; +import { CategoriesDisplay } from "./categories-display"; +import { HoldingsTable } from "./holdings-table"; +import { Investmentcards } from "./investment-cards"; +import { Nav } from "./nav"; +import { SmallInvestmentCard } from "./small-investment-card"; +import { SpentSoFarCard } from "./total-balance-card"; + +interface MailProps { + accounts: { + label: string; + email: string; + icon: React.ReactNode; + }[]; + mails: Mail[]; + defaultLayout: number[] | undefined; + defaultCollapsed?: boolean; + navCollapsedSize: number; +} + +export function CategoriesDashboard({ + accounts, + mails, + defaultLayout = [265, 440, 400], + defaultCollapsed = false, + navCollapsedSize, +}: MailProps) { + const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed); + const [mail] = useMail(); + + return ( + + { + document.cookie = `react-resizable-panels:layout=${JSON.stringify( + sizes, + )}`; + }} + className="h-full max-h-[1200px] items-stretch" + > + { + setIsCollapsed(collapsed); + document.cookie = `react-resizable-panels:collapsed=${JSON.stringify( + collapsed, + )}`; + }} + className={cn( + isCollapsed && + "min-w-[50px] transition-all duration-300 ease-in-out", + )} + > +
+ +
+ +