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
116 changes: 57 additions & 59 deletions app/(protected)/raw-deals/page.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,74 @@
// app/(protected)/raw-deals/page.tsx
import React, { Suspense } from "react";
import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton";
import { Metadata } from "next";
import prismaDB from "@/lib/prisma";
import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton";
import DealCard from "@/components/DealCard";
import getCurrentUserRole from "@/lib/data/current-user-role";
import GetDeals, { GetAllDeals } from "@/app/actions/get-deal";
import SearchDeals from "@/components/SearchDeal";
import Pagination from "@/components/pagination";
import { setTimeout } from "timers/promises";
import DealTypeFilter from "@/components/DealTypeFilter";
import { DealType } from "@prisma/client";
import SearchDealsSkeleton from "@/components/skeletons/SearchDealsSkeleton";
import SearchEbitdaDeals from "@/components/SearchEbitdaDeals";
import DealTypeFilterSkeleton from "@/components/skeletons/DealTypeFilterSkeleton";
import UserDealFilter from "@/components/UserDealFilter";
import DealTypeFilter from "@/components/DealTypeFilter";
import Pagination from "@/components/pagination";
import getCurrentUserRole from "@/lib/data/current-user-role";
import { DealType } from "@prisma/client";
import { GetAllDeals } from "@/app/actions/get-deal";
import DealContainer from "@/components/DealContainer";

export const metadata: Metadata = {
title: "Raw Deals",
description: "View the raw deals",
title: "Inferred Deals",
description: "View the inferred deals scraped using AI",
};

// After
type SearchParams = Promise<{ [key: string]: string | undefined }>;
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>;

const RawDealsPage = async (props: { searchParams: SearchParams }) => {
const searchParams = await props.searchParams;
const search = searchParams?.query || "";
const currentPage = Number(searchParams?.page) || 1;
const limit = Number(searchParams?.limit) || 20;
export default async function RawDealsPage(props: {
searchParams: SearchParams;
}) {
const params = await props.searchParams;

const search = Array.isArray(params.query)
? params.query[0]
: params.query ?? "";
const currentPage =
Number(Array.isArray(params.page) ? params.page[0] : params.page) || 1;
const limit =
Number(Array.isArray(params.limit) ? params.limit[0] : params.limit) || 20;
const offset = (currentPage - 1) * limit;
const ebitda =
Array.isArray(params.ebitda) ? params.ebitda[0] : params.ebitda ?? "";

const ebitda = searchParams?.ebitda || "";
const userId = searchParams?.userId || "";
// Ensure dealTypes is always an array
const dealTypes =
typeof searchParams?.dealType === "string"
? [searchParams.dealType]
: searchParams?.dealType || [];
const rawDealTypes = Array.isArray(params.dealType)
? params.dealType
: params.dealType
? [params.dealType]
: [];
const dealTypes = rawDealTypes as DealType[];

const { data, totalPages, totalCount } = await GetAllDeals({
search,
offset,
limit,
dealTypes: dealTypes as DealType[],
dealTypes,
ebitda,
userId,
});

const currentUserRole = await getCurrentUserRole();

return (
<section className="block-space group container">
<div className="mb-8 text-center">
<h1 className="mb-4 text-4xl font-bold md:mb-6 lg:mb-8">Raw Deals</h1>
<h1 className="mb-4 text-4xl font-bold">Raw Deals</h1>
<p className="mx-auto max-w-2xl text-lg text-muted-foreground">
Browse through our collection of unprocessed deals gathered from
various sources including manual entries, bulk uploads, external
website scraping, and AI-inferred opportunities.
Browse our unprocessed deals from manual entries, bulk uploads,
web-scraping and AI-driven inferences.
</p>
</div>

<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-2">
<h4 className="text-lg font-medium">
Total Deals: <span className="font-bold">{totalCount}</span>
<div className="flex items-center gap-4">
<h4 className="text-lg">
Total Deals: <strong>{totalCount}</strong>
</h4>
<div className="ml-4 rounded-md bg-primary/10 px-3 py-1 text-sm">
<div className="rounded-md bg-primary/10 px-3 py-1 text-sm">
Page {currentPage} of {totalPages}
</div>
</div>
Expand All @@ -77,33 +80,28 @@ const RawDealsPage = async (props: { searchParams: SearchParams }) => {
<Suspense fallback={<SearchDealsSkeleton />}>
<SearchEbitdaDeals />
</Suspense>
</div>
<Suspense fallback={<DealTypeFilterSkeleton />}>
<DealTypeFilter />
</Suspense>
<Suspense fallback={<DealTypeFilterSkeleton />}>
<UserDealFilter />
</Suspense>
</div>
</div>

<div className="group-has-[[data-pending]]:animate-pulse">
{data.length === 0 ? (
<div className="mt-12 text-center">
<p className="text-xl text-muted-foreground">
No deals found matching your criteria.
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{data.map((e) => (
<DealCard key={e.id} deal={e} userRole={currentUserRole!} />
))}
</div>
)}
</div>
{data.length === 0 ? (
<div className="mt-12 text-center">
<p className="text-xl text-muted-foreground">
No deals found matching your criteria.
</p>
</div>
) : (
<DealContainer
data={data}
userRole={currentUserRole!}
currentPage={currentPage}
totalPages={totalPages}
totalCount={totalCount}
/>
)}

<Pagination totalPages={totalPages} />
</section>
);
};
}

export default RawDealsPage;
136 changes: 136 additions & 0 deletions components/DealContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Grid, List } from "lucide-react";
import DealCard from "@/components/DealCard";
import DealListItem from "@/components/DealListItem";
import type { Deal, UserRole } from "@prisma/client";
import DeleteDealFromDB from "@/app/actions/delete-deal";

interface DealContainerProps {
data: Deal[];
userRole: UserRole;
currentPage: number;
totalPages: number;
totalCount: number;
}

export default function DealContainer({
data,
userRole,
}: DealContainerProps) {
const router = useRouter();
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

const allSelected = data.length > 0 && selectedIds.size === data.length;

function toggleAll() {
setSelectedIds(allSelected ? new Set() : new Set(data.map(d => d.id)));
}
function toggleOne(id: string) {
setSelectedIds(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}

async function handleBulkDelete() {
for (const id of selectedIds) {
const deal = data.find(d => d.id === id);
if (deal) await DeleteDealFromDB(deal.dealType, id);
}
router.refresh();
setSelectedIds(new Set());
}

function handleBulkScreen() {
if (!selectedIds.size) return;
const ids = Array.from(selectedIds).join(",");
router.push(`/raw-deals/screen?ids=${ids}`);
}

return (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex space-x-2">
<button
onClick={() => setViewMode("grid")}
className={`p-2 rounded ${
viewMode === "grid" ? "bg-gray-700" : "hover:bg-gray-800"
}`}
>
<Grid className="w-5 h-5 text-white" />
</button>
<button
onClick={() => setViewMode("list")}
className={`p-2 rounded ${
viewMode === "list" ? "bg-gray-700" : "hover:bg-gray-800"
}`}
>
<List className="w-5 h-5 text-white" />
</button>
</div>

{viewMode === "list" && (
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2 text-white">
<input
type="checkbox"
checked={allSelected}
onChange={toggleAll}
className="h-4 w-4 rounded border-gray-600 bg-gray-800 text-primary focus:ring-0"
/>
<span>Select All</span>
</label>

<button
onClick={handleBulkDelete}
disabled={!selectedIds.size}
className={`px-4 py-2 rounded ${
selectedIds.size
? "bg-red-600 text-white hover:bg-red-700"
: "bg-gray-700 text-gray-400 cursor-not-allowed"
}`}
>
Delete Selected
</button>

<button
onClick={handleBulkScreen}
disabled={!selectedIds.size}
className={`px-4 py-2 rounded ${
selectedIds.size
? "bg-green-600 text-white hover:bg-green-700"
: "bg-gray-700 text-gray-400 cursor-not-allowed"
}`}
>
Screen Selected
</button>
</div>
)}
</div>

{viewMode === "grid" ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{data.map(deal => (
<DealCard key={deal.id} deal={deal} userRole={userRole} />
))}
</div>
) : (
<div className="flex flex-col divide-y divide-gray-800">
{data.map(deal => (
<DealListItem
key={deal.id}
deal={deal}
selected={selectedIds.has(deal.id)}
onToggle={() => toggleOne(deal.id)}
/>
))}
</div>
)}
</>
);
}

49 changes: 49 additions & 0 deletions components/DealListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import Link from "next/link";
import type { Deal } from "@prisma/client";

interface Props {
deal: Deal;
selected: boolean;
onToggle: () => void;
}

export default function DealListItem({ deal, selected, onToggle }: Props) {
return (
<div className="flex items-center justify-between px-4 py-3 hover:bg-gray-800">
<div className="flex items-center flex-1 space-x-3">
<input
type="checkbox"
checked={selected}
onChange={onToggle}
className="h-4 w-4 rounded border-gray-600 bg-gray-800 text-primary focus:ring-0"
/>
<div>
<h3 className="text-lg font-semibold text-white">
{deal.dealCaption}
</h3>
<div className="text-sm text-gray-400">
Revenue: ${deal.revenue} · EBITDA: ${deal.ebitda} ·{" "}
{deal.industry || "—"}
</div>
</div>
</div>

<div className="flex-shrink-0 flex space-x-2">
<Link
href={`/raw-deals/${deal.id}`}
className="px-4 py-2 bg-gray-100 text-black rounded"
>
View Details
</Link>
<Link
href={`/raw-deals/${deal.id}/screen`}
className="px-4 py-2 border border-gray-600 text-gray-300 rounded"
>
Screen Deal
</Link>
</div>
</div>
);
}