Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/(protected)/notifications/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Notification } from "@prisma/client";
import { ColumnDef } from "@tanstack/react-table";

function timeSince(date: Date) {
var seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);

var interval = seconds / 31536000;

if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}

export const columns: ColumnDef<Notification>[] = [
{
accessorKey: "dealTitle",
header: "Deal Title",
cell: ({ row }) => (
<a className="underline" href={"/raw-deals/" + row.original.dealId}>{row.original.dealTitle}</a>
),
// enableSorting: true,
enableColumnFilter: true,
filterFn: 'includesString',
enableHiding: false,
},
{
accessorKey: "createdAt",
header: "Queued",
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{timeSince(row.original.createdAt)} ago
</span>
),
},
];
154 changes: 154 additions & 0 deletions app/(protected)/notifications/data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";

import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import * as React from "react";
// Intersection Observer hook
function useVisibleRows(rowCount: number, onVisible: (maxIndex: number) => void) {
const rowRefs = React.useRef<(HTMLTableRowElement | null)[]>([]);
React.useEffect(() => {
const observer = new window.IntersectionObserver(
(entries) => {
const visible: number[] = [];
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = Number((entry.target as HTMLElement).dataset.rowindex);
if (!isNaN(idx)) visible.push(idx);
}
});
if (visible.length > 0) {
onVisible(Math.max(...visible) + 1);
}
},
{ threshold: 0.1 }
);
rowRefs.current.forEach((ref) => {
if (ref) observer.observe(ref);
});
return () => {
observer.disconnect();
};
}, [rowCount, onVisible]);
return rowRefs;
}
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Input } from "@/components/ui/input";

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}

export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [maxPageSize, setMaxPageSize] = React.useState(25);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: {
pagination: {
pageIndex: 0,
pageSize: maxPageSize, //custom default page size
},
},
});

// Intersection Observer logic
const rows = table.getRowModel().rows;
const rowRefs = useVisibleRows(rows.length, (maxIdx) => {
setMaxPageSize((prev) => {
return Math.max(maxIdx + 25, prev);
});
});

return (
<div className="text-right">
<div className="flex py-4">
<Input
placeholder="Filter Deal Titles..."
value={
(table.getColumn("dealTitle")?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table.getColumn("dealTitle")?.setFilterValue(event.target.value)
}
className="w-full"
/>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="text-right" key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row: any, idx: number) => {
return (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
data-rowindex={idx}
ref={(el: HTMLTableRowElement | null) => {
rowRefs.current[idx] = el;
}}
>
{row.getVisibleCells().map((cell: any) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
);
})
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}
118 changes: 118 additions & 0 deletions app/(protected)/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client";

import {
getNotifications,
markNotificationAsSeen,
} from "@/app/actions/notifications";
import useNotifications from "@/hooks/use-notifications";
import { cn } from "@/lib/utils";
import { Notification } from "@prisma/client";
import { Circle, Dot, TrendingUp } from "lucide-react";
import { useSession } from "next-auth/react";
import { useState, useTransition, useRef, useCallback, useEffect } from "react";
import { DataTable } from "./data-table";
import { columns } from "./columns";

type PendingDeal = {
id: string;
title: string;
ebitda: number;
status: string;
};

type WebSocketMessage = {
type: string;
productId?: string;
status?: string;
userId?: string;
};

export default function Notifications() {
const { notifications, wsConnected } = useNotifications();
const pendingDeals = notifications.filter(
(notif) => notif.status === "PENDING",
);
const completedDeals = notifications.filter(
(notif) => notif.status === "COMPLETED",
);


if (!wsConnected)
return <div className="block-space big-container flex-1">Loading...</div>;

return (
<div className="block-space big-container flex flex-row">
<div className="flex-1">
<h3>Pending - {pendingDeals.length}</h3>
{/* <div className="flex flex-col gap-y-4">
{pendingDeals.map((deal) => (
<div key={deal.id}>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<h5 className="truncate text-sm font-medium text-foreground">
{deal.dealTitle || `Deal #${deal.dealId}`}
</h5>
<p className="mt-1 text-xs text-muted-foreground">
ID: {deal.id}
</p>
</div>
</div>
</div>
))}
</div> */}
<div className="flex flex-col items-start gap-y-4 text-left">
{pendingDeals.map((deal) => (
<div
key={deal.id}
className="flex w-fit flex-row items-center gap-4 rounded-md border-2 p-2"
>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<h5 className="truncate text-sm font-medium text-foreground">
{deal.dealTitle || `Deal #${deal.id}`}
</h5>
<p className="mt-1 text-xs text-muted-foreground">
ID: {deal.dealId}
</p>
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex-1 text-right">
<h3 className="mb-2">Completed - {completedDeals.length}</h3>
<DataTable columns={columns} data={completedDeals} />
{/* <div className="flex flex-col items-end gap-y-4 text-left">
{completedDeals.map((deal) => (
<a
onMouseEnter={async () => {
console.log(deal.seen);
await markNotificationAsSeen(deal.id);
}}
key={deal.id}
href={`/raw-deals/${deal.dealId}`}
className={cn(
"flex w-fit flex-row items-center gap-4 rounded-md border-2 p-2",
{
"bg-red-100 font-semibold": !deal.seen,
},
)}
>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<h5 className="truncate text-sm font-medium text-foreground">
{deal.dealTitle || `Deal #${deal.id}`}
</h5>
<p className="mt-1 text-xs text-muted-foreground">
ID: {deal.dealId}
</p>
</div>
</div>
</a>
))}
</div> */}
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions app/actions/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use server";

import prismaDB from "@/lib/prisma";

export async function getNotifications(userId: string) {
const notifications = await prismaDB.notification.findMany({
where: {
userId: userId,
},
orderBy: {
createdAt: 'desc'
}
});
return notifications;
}

export async function markNotificationAsSeen(notificationId: string) {
await prismaDB.notification.update({
where: {
id: notificationId,
},
data: {
seen: true,
},
});
}
Loading