mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 21:44:06 +01:00
Merge branch 'front-end' into back-end
This commit is contained in:
commit
0ff7234c2b
43
package-lock.json
generated
43
package-lock.json
generated
@ -39,6 +39,7 @@
|
|||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-query-devtools": "^5.59.0",
|
"@tanstack/react-query-devtools": "^5.59.0",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"b2d-ventures": "file:",
|
"b2d-ventures": "file:",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.6",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@ -47,7 +48,7 @@
|
|||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
"framer-motion": "^11.11.15",
|
"framer-motion": "^11.11.17",
|
||||||
"lucide-react": "^0.428.0",
|
"lucide-react": "^0.428.0",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.15",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -4212,6 +4213,39 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
|
||||||
|
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.20.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||||
|
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
||||||
@ -7916,9 +7950,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "11.11.15",
|
"version": "11.11.17",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.15.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.17.tgz",
|
||||||
"integrity": "sha512-fib+OpUe4nap4W1vhaDohnPU+42B5s1q+jNkNkb6TfzmDYtRhDyvn4mkRCAD5a2cQKOI3nJZP7yqGYtxnlmleg==",
|
"integrity": "sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-query-devtools": "^5.59.0",
|
"@tanstack/react-query-devtools": "^5.59.0",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"b2d-ventures": "file:",
|
"b2d-ventures": "file:",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.6",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@ -49,7 +50,7 @@
|
|||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
"framer-motion": "^11.11.15",
|
"framer-motion": "^11.11.17",
|
||||||
"lucide-react": "^0.428.0",
|
"lucide-react": "^0.428.0",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.15",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { StaticImport } from "next/dist/shared/lib/get-img-props";
|
|
||||||
|
|
||||||
interface ItemProps {
|
|
||||||
src: string | StaticImport;
|
|
||||||
alt: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageModal = ({ src, alt, width, height, className }: ItemProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Image src={src} alt={alt} width={width} height={height} className={className} />
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Image Preview</DialogTitle>
|
|
||||||
<DialogDescription>Click outside to close the image preview.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Image src={src} alt={alt} width={700} height={400} />
|
|
||||||
<DialogFooter />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DisplayFullImage({ src, alt, width, height, className }: ItemProps) {
|
|
||||||
return <ImageModal src={src} alt={alt} width={width} height={height} className={className} />;
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import CustomTooltip from "@/components/customToolTip";
|
||||||
import { ShareIcon, StarIcon } from "lucide-react";
|
import { ShareIcon, StarIcon } from "lucide-react";
|
||||||
import { deleteFollow, getFollow, insertFollow } from "@/lib/data/followQuery";
|
import { deleteFollow, getFollow, insertFollow } from "@/lib/data/followQuery";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@ -13,9 +13,10 @@ import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
|||||||
interface FollowShareButtons {
|
interface FollowShareButtons {
|
||||||
userId: string;
|
userId: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
projectName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FollowShareButtons = ({ userId, projectId }: FollowShareButtons) => {
|
const FollowShareButtons = ({ userId, projectId, projectName }: FollowShareButtons) => {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
const { data: follow, isLoading: followIsLoading } = useQuery(getFollow(supabase, userId, projectId), {
|
const { data: follow, isLoading: followIsLoading } = useQuery(getFollow(supabase, userId, projectId), {
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
@ -71,16 +72,9 @@ const FollowShareButtons = ({ userId, projectId }: FollowShareButtons) => {
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-5 justify-self-end ">
|
<div className="grid grid-cols-2 gap-5 justify-self-end ">
|
||||||
<div className="mt-2 cursor-pointer" onClick={handleFollow}>
|
<div className="mt-2 cursor-pointer" onClick={handleFollow}>
|
||||||
<TooltipProvider>
|
<CustomTooltip message={`Follow ${projectName}`}>
|
||||||
<Tooltip>
|
<StarIcon id="follow" fill={isFollowState ? "#fcb30e" : "#fff"} strokeWidth={2} />
|
||||||
<TooltipTrigger asChild>
|
</CustomTooltip>
|
||||||
<StarIcon id="follow" fill={isFollowState ? "#fcb30e" : "#fff"} strokeWidth={2} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Follow NVIDIA</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
<div onClick={handleShare} className="cursor-pointer mt-2">
|
<div onClick={handleShare} className="cursor-pointer mt-2">
|
||||||
<ShareIcon />
|
<ShareIcon />
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||||
@ -10,7 +8,6 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
import FollowShareButtons from "./followShareButton";
|
import FollowShareButtons from "./followShareButton";
|
||||||
|
|
||||||
import { getProjectData } from "@/lib/data/projectQuery";
|
import { getProjectData } from "@/lib/data/projectQuery";
|
||||||
import { getDealList } from "@/app/api/dealApi";
|
import { getDealList } from "@/app/api/dealApi";
|
||||||
import { sumByKey, toPercentage } from "@/lib/utils";
|
import { sumByKey, toPercentage } from "@/lib/utils";
|
||||||
@ -89,7 +86,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
|
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
|
||||||
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
|
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
|
||||||
</span>
|
</span>
|
||||||
<FollowShareButtons userId={user!.user.id} projectId={params.id} />
|
<FollowShareButtons userId={user!.user.id} projectId={params.id} projectName={projectData?.project_name} />
|
||||||
</div>
|
</div>
|
||||||
{/* end of pack */}
|
{/* end of pack */}
|
||||||
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
|
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import { overAllGraphData, fourYearGraphData, dayOftheWeekData } from "../portfo
|
|||||||
import CountUp from "react-countup";
|
import CountUp from "react-countup";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Modal } from "@/components/modal";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
@ -50,17 +52,17 @@ export default function Dashboard() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
let graphData = [];
|
let graphData = [];
|
||||||
const filteredData = (investmentDetail?.data || []).filter((deal) => deal.project_id === currentProjectId);
|
const filteredProject = (investmentDetail?.data || []).filter((deal) => deal.project_id === currentProjectId);
|
||||||
const handleTabChange = (tab: string) => {
|
const handleTabChange = (tab: string) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeTab === "daily") {
|
if (activeTab === "daily") {
|
||||||
graphData = dayOftheWeekData(filteredData);
|
graphData = dayOftheWeekData(filteredProject);
|
||||||
} else if (activeTab === "yearly") {
|
} else if (activeTab === "yearly") {
|
||||||
graphData = fourYearGraphData(filteredData);
|
graphData = fourYearGraphData(filteredProject);
|
||||||
} else {
|
} else {
|
||||||
graphData = overAllGraphData(filteredData);
|
graphData = overAllGraphData(filteredProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -116,193 +118,191 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl">
|
<div className="container max-w-screen-xl">
|
||||||
<Loader isSuccess={!isLoadingSession && !isLoadingProjects} />{" "}
|
<Loader isSuccess={!isLoadingSession && !isLoadingProjects} />{" "}
|
||||||
<div className="md:hidden">
|
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||||
<Image
|
<div className="flex items-center justify-between space-y-2">
|
||||||
src="/examples/dashboard-light.png"
|
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
|
||||||
width={1280}
|
</div>
|
||||||
height={866}
|
{projects && projects.length > 0 && (
|
||||||
alt="Dashboard"
|
<Tabs className="space-y-4" defaultValue={projects[0].project_name}>
|
||||||
className="block dark:hidden"
|
<TabsList>
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src="/examples/dashboard-dark.png"
|
|
||||||
width={1280}
|
|
||||||
height={866}
|
|
||||||
alt="Dashboard"
|
|
||||||
className="hidden dark:block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="hidden flex-col md:flex">
|
|
||||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
|
||||||
<div className="flex items-center justify-between space-y-2">
|
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
|
|
||||||
</div>
|
|
||||||
{projects && projects.length > 0 && (
|
|
||||||
<Tabs className="space-y-4" defaultValue={projects[0].project_name}>
|
|
||||||
<TabsList>
|
|
||||||
{projects.map((project) => (
|
|
||||||
<TabsTrigger
|
|
||||||
key={project.id}
|
|
||||||
value={project.project_name}
|
|
||||||
onClick={() => setCurrentProjectId(project.id)}
|
|
||||||
>
|
|
||||||
{project.project_name}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<TabsContent value={project.project_name} className="space-y-4" key={project.id}>
|
<TabsTrigger
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
key={project.id}
|
||||||
<Card>
|
value={project.project_name}
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
onClick={() => setCurrentProjectId(project.id)}
|
||||||
<CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle>
|
>
|
||||||
<svg
|
{project.project_name}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</TabsTrigger>
|
||||||
viewBox="0 0 24 24"
|
))}
|
||||||
fill="none"
|
</TabsList>
|
||||||
stroke="currentColor"
|
{projects.map((project) => (
|
||||||
strokeLinecap="round"
|
<TabsContent value={project.project_name} className="space-y-4" key={project.id}>
|
||||||
strokeLinejoin="round"
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
strokeWidth="2"
|
<Card>
|
||||||
className="h-4 w-4 text-muted-foreground"
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
>
|
<CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle>
|
||||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
<svg
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</CardHeader>
|
viewBox="0 0 24 24"
|
||||||
<CardContent>
|
fill="none"
|
||||||
<div className="text-2xl font-bold">
|
stroke="currentColor"
|
||||||
$
|
strokeLinecap="round"
|
||||||
<CountUp
|
strokeLinejoin="round"
|
||||||
end={filteredData
|
strokeWidth="2"
|
||||||
.filter((project) => project.deal_status === "Completed")
|
className="h-4 w-4 text-muted-foreground"
|
||||||
.reduce((sum, current) => sum + current.deal_amount, 0)}
|
>
|
||||||
duration={1}
|
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||||
/>
|
</svg>
|
||||||
</div>
|
</CardHeader>
|
||||||
</CardContent>
|
<CardContent>
|
||||||
</Card>
|
<div className="text-2xl font-bold">
|
||||||
<Card>
|
$
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CountUp
|
||||||
<CardTitle className="text-sm font-medium">Profile Views</CardTitle>
|
end={filteredProject
|
||||||
<svg
|
.filter((project) => project.deal_status === "Completed")
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
.reduce((sum, current) => sum + current.deal_amount, 0)}
|
||||||
viewBox="0 0 24 24"
|
duration={1}
|
||||||
fill="none"
|
/>
|
||||||
stroke="currentColor"
|
</div>
|
||||||
strokeLinecap="round"
|
</CardContent>
|
||||||
strokeLinejoin="round"
|
</Card>
|
||||||
strokeWidth="2"
|
<Card>
|
||||||
className="h-4 w-4 text-muted-foreground"
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
>
|
<CardTitle className="text-sm font-medium">Profile Views</CardTitle>
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
<svg
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
viewBox="0 0 24 24"
|
||||||
</CardHeader>
|
fill="none"
|
||||||
<CardContent>
|
stroke="currentColor"
|
||||||
<div className="text-2xl font-bold">
|
strokeLinecap="round"
|
||||||
+<CountUp end={2350} duration={1} />
|
strokeLinejoin="round"
|
||||||
</div>
|
strokeWidth="2"
|
||||||
{/* <p className="text-xs text-muted-foreground">
|
className="h-4 w-4 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
+<CountUp end={2350} duration={1} />
|
||||||
|
</div>
|
||||||
|
{/* <p className="text-xs text-muted-foreground">
|
||||||
+180.1% from last month
|
+180.1% from last month
|
||||||
</p> */}
|
</p> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Total Followers</CardTitle>
|
<CardTitle className="text-sm font-medium">Total Followers</CardTitle>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-4 w-4 text-muted-foreground"
|
className="h-4 w-4 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
|
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">
|
||||||
+<CountUp end={12234} duration={1} />
|
+<CountUp end={12234} duration={1} />
|
||||||
</div>
|
</div>
|
||||||
{/* <p className="text-xs text-muted-foreground">
|
{/* <p className="text-xs text-muted-foreground">
|
||||||
+19% from last month
|
+19% from last month
|
||||||
</p> */}
|
</p> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`/project/${project.id}/edit`);
|
router.push(`/project/${project.id}/edit`);
|
||||||
}}
|
}}
|
||||||
className="h-full bg-emerald-500 hover:bg-emerald-800 font-bold text-xl"
|
className="h-full bg-emerald-500 hover:bg-emerald-800 font-bold text-xl"
|
||||||
>
|
>
|
||||||
Edit Project
|
Edit Project
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
d="M3 17.25V21h3.75l11.05-11.05-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 000-1.42l-2.34-2.34a1.003 1.003 0 00-1.42 0L15.13 4.5l3.75 3.75 1.83-1.21z"
|
d="M3 17.25V21h3.75l11.05-11.05-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 000-1.42l-2.34-2.34a1.003 1.003 0 00-1.42 0L15.13 4.5l3.75 3.75 1.83-1.21z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||||
<Card className="col-span-4">
|
<Card className="col-span-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Overview</CardTitle>
|
<CardTitle>Overview</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||||
<TabsList className="grid w-56 grid-cols-3 ml-5">
|
<TabsList className="grid w-56 grid-cols-3 ml-5">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
<TabsTrigger key={tab} value={tab}>
|
<TabsTrigger key={tab} value={tab}>
|
||||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<CardContent className="pl-2 mt-5">
|
<CardContent className="pl-2 mt-5">
|
||||||
<Overview graphType={graphType} data={graphData} />
|
<Overview graphType={graphType} data={graphData} />
|
||||||
|
|
||||||
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
|
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
|
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
|
||||||
Line
|
Line
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
|
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
|
||||||
Bar
|
Bar
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
<Card className="col-span-3">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recent Funds</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<RecentFunds
|
|
||||||
data={latestInvestment.map((item) => {
|
|
||||||
return {
|
|
||||||
name: item.username,
|
|
||||||
amount: item.dealAmount,
|
|
||||||
avatar: item.avatarUrl,
|
|
||||||
date: new Date(item.createdTime),
|
|
||||||
status: item.dealStatus,
|
|
||||||
profile_url: `/profile/${item.investorId}`,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Tabs>
|
||||||
</div>
|
</Card>
|
||||||
</TabsContent>
|
<Card className="col-span-4 md:col-span-3">
|
||||||
))}
|
<CardHeader>
|
||||||
</Tabs>
|
<CardTitle>Recent Funds</CardTitle>
|
||||||
)}
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="grid grid-flow-dense w-full">
|
||||||
|
<RecentFunds
|
||||||
|
data={latestInvestment.map((item) => {
|
||||||
|
return {
|
||||||
|
name: item.username,
|
||||||
|
amount: item.dealAmount,
|
||||||
|
avatar: item.avatarUrl,
|
||||||
|
date: new Date(item.createdTime),
|
||||||
|
status: item.dealStatus,
|
||||||
|
profile_url: `/profile/${item.investorId}`,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-center mt-5">
|
||||||
|
{filteredProject && filteredProject.length > 1 ? (
|
||||||
|
<Modal
|
||||||
|
data={filteredProject.map((item) => {
|
||||||
|
return {
|
||||||
|
date: item.created_time,
|
||||||
|
name: item.username,
|
||||||
|
amount: item.deal_amount,
|
||||||
|
status: item.deal_status,
|
||||||
|
logoURL: Array.isArray(item.avatar_url) ? item.avatar_url[0] : item.avatar_url,
|
||||||
|
profileURL: `/profile/${item.investor_id}`,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,6 +24,10 @@ import { NoDataAlert } from "@/components/alert/noData/alert";
|
|||||||
import { error } from "console";
|
import { error } from "console";
|
||||||
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
|
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { DataTable } from "@/components/dataTable";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import CustomTooltip from "@/components/customToolTip";
|
||||||
|
import { Modal } from "@/components/modal";
|
||||||
|
|
||||||
export default async function Portfolio({ params }: { params: { uid: string } }) {
|
export default async function Portfolio({ params }: { params: { uid: string } }) {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
@ -53,6 +57,7 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
if (investorDealError) {
|
if (investorDealError) {
|
||||||
console.error(investorDealError);
|
console.error(investorDealError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: localUser, error: localUserError } = await supabase.auth.getUser();
|
const { data: localUser, error: localUserError } = await supabase.auth.getUser();
|
||||||
if (localUserError) {
|
if (localUserError) {
|
||||||
console.error("Error while fetching user" + error);
|
console.error("Error while fetching user" + error);
|
||||||
@ -72,7 +77,16 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
const tags = deals ? await getInvestorProjectTag(supabase, deals) : [];
|
const tags = deals ? await getInvestorProjectTag(supabase, deals) : [];
|
||||||
const latestDeals = deals
|
const latestDeals = deals
|
||||||
? await Promise.all(
|
? await Promise.all(
|
||||||
(await getLatestInvestment(supabase, deals)).map(async (deal) => ({
|
(
|
||||||
|
await getLatestInvestment(
|
||||||
|
supabase,
|
||||||
|
deals.map((deal) => ({
|
||||||
|
...deal,
|
||||||
|
status: deal.deal_status,
|
||||||
|
project_id: deal.project_id,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
).map(async (deal) => ({
|
||||||
...deal,
|
...deal,
|
||||||
logo_url: await deal.logo_url,
|
logo_url: await deal.logo_url,
|
||||||
}))
|
}))
|
||||||
@ -96,7 +110,6 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
<CountUpComponent end={totalInvestment} duration={1} />
|
<CountUpComponent end={totalInvestment} duration={1} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flew-rows-3 gap-10 mt-5 w-full">
|
<div className="flex flew-rows-3 gap-10 mt-5 w-full">
|
||||||
<Tabs defaultValue="daily" className="space-y-4 w-full">
|
<Tabs defaultValue="daily" className="space-y-4 w-full">
|
||||||
<TabsList className="grid w-96 grid-cols-3">
|
<TabsList className="grid w-96 grid-cols-3">
|
||||||
@ -175,8 +188,8 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-cols-3 w-full gap-5 mt-5">
|
<div className="grid grid-cols-1 md:grid-cols-3 w-full gap-5 mt-5">
|
||||||
<Card className="w-1/3">
|
<Card className="w-full h-fit">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-md font-bold">Categories of Invested Projects</CardTitle>
|
<CardTitle className="text-md font-bold">Categories of Invested Projects</CardTitle>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -201,7 +214,7 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-1/3">
|
<Card className="w-full h-fit">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-md font-bold">Types of Businesses Invested In</CardTitle>
|
<CardTitle className="text-md font-bold">Types of Businesses Invested In</CardTitle>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -226,12 +239,39 @@ export default async function Portfolio({ params }: { params: { uid: string } })
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-1/3">
|
<Card className="w-full">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-md font-bold">Recent investment</CardTitle>
|
<CardTitle className="text-md font-bold">Recent investment</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="mt-5">
|
<CardContent className="mt-5 grid grid-flow-row-dense">
|
||||||
<RecentFunds data={latestDeals} />
|
<RecentFunds
|
||||||
|
data={latestDeals.map((item) => {
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
amount: item.amount,
|
||||||
|
avatar: item.logo_url,
|
||||||
|
date: new Date(item.date),
|
||||||
|
status: item.status,
|
||||||
|
profile_url: `/deals/${item.projectId}`,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div className="mt-5 flex justify-center">
|
||||||
|
{deals && deals.length > 5 ? (
|
||||||
|
<Modal
|
||||||
|
data={deals.map((item) => {
|
||||||
|
return {
|
||||||
|
date: item.created_time,
|
||||||
|
name: item.project_name,
|
||||||
|
amount: item.deal_amount,
|
||||||
|
status: item.deal_status,
|
||||||
|
logoURL: Array.isArray(item.avatar_url) ? item.avatar_url[0] : item.avatar_url,
|
||||||
|
profileURL: `/deals/${item.project_id}`,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ function getTotalInvestment(deals: { deal_amount: number }[]) {
|
|||||||
}
|
}
|
||||||
async function getLatestInvestment(
|
async function getLatestInvestment(
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
deals: { project_id: number; deal_amount: number; created_time: Date;}[]
|
deals: { project_id: number; deal_amount: number; created_time: Date; status: string }[]
|
||||||
) {
|
) {
|
||||||
const llist = [];
|
const llist = [];
|
||||||
const count = 5;
|
const count = 5;
|
||||||
@ -43,6 +43,7 @@ async function getLatestInvestment(
|
|||||||
amount: deals[i].deal_amount,
|
amount: deals[i].deal_amount,
|
||||||
date: new Date(deals[i].created_time),
|
date: new Date(deals[i].created_time),
|
||||||
logo_url: url,
|
logo_url: url,
|
||||||
|
status: deals[i].status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function ProjectSection({ projectsData }: { projectsData: ProjectCardProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-4 gap-6">
|
||||||
{projectsData.map((project) => (
|
{projectsData.map((project) => (
|
||||||
<div key={project.id}>
|
<div key={project.id}>
|
||||||
<Link href={`/deals/${project.id}`}>
|
<Link href={`/deals/${project.id}`}>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
|
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
@ -8,15 +8,40 @@ interface GalleryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Gallery = ({ images }: GalleryProps) => {
|
const Gallery = ({ images }: GalleryProps) => {
|
||||||
const [mainApi, setMainApi] = useState<CarouselApi>();
|
const [mainApi, setMainApi] = useState<CarouselApi | null>(null);
|
||||||
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi>();
|
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi | null>(null);
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
const syncCarousels = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (mainApi && thumbnailApi) {
|
||||||
|
setCurrent(index);
|
||||||
|
mainApi.scrollTo(index);
|
||||||
|
thumbnailApi.scrollTo(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mainApi, thumbnailApi]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
syncCarousels(index);
|
||||||
|
},
|
||||||
|
[syncCarousels]
|
||||||
|
);
|
||||||
|
|
||||||
const mainImage = useMemo(
|
const mainImage = useMemo(
|
||||||
() =>
|
() =>
|
||||||
images.map((image, index) => (
|
images.map((image, index) => (
|
||||||
<CarouselItem key={index} className="relative aspect-video w-full border-8 border-b">
|
<CarouselItem key={index} className="relative aspect-video w-full border-8 border-b">
|
||||||
<Image src={image.src} alt={`Carousel Main Image ${index + 1}`} fill style={{ objectFit: "contain" }} />
|
<Image
|
||||||
|
src={image.src}
|
||||||
|
alt={`Carousel Main Image ${index + 1}`}
|
||||||
|
fill
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
priority={index === 0}
|
||||||
|
/>
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
)),
|
)),
|
||||||
[images]
|
[images]
|
||||||
@ -25,61 +50,60 @@ const Gallery = ({ images }: GalleryProps) => {
|
|||||||
const thumbnailImages = useMemo(
|
const thumbnailImages = useMemo(
|
||||||
() =>
|
() =>
|
||||||
images.map((image, index) => (
|
images.map((image, index) => (
|
||||||
<CarouselItem key={index} className="relative aspect-square basis-1/4" onClick={() => handleClick(index)}>
|
<CarouselItem
|
||||||
|
key={index}
|
||||||
|
className="relative aspect-square basis-1/4 cursor-pointer"
|
||||||
|
onClick={() => handleClick(index)}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
className={`${index === current ? "border-2" : ""}`}
|
className={`transition-all duration-200 ${index === current ? "border-2 border-primary" : ""}`}
|
||||||
src={image.src}
|
src={image.src}
|
||||||
fill
|
fill
|
||||||
alt={`Carousel Thumbnail Image ${index + 1}`}
|
alt={`Carousel Thumbnail Image ${index + 1}`}
|
||||||
style={{ objectFit: "contain" }}
|
style={{ objectFit: "contain" }}
|
||||||
|
priority={index === 0}
|
||||||
/>
|
/>
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
)),
|
)),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[images, current, handleClick]
|
||||||
[images, current]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mainApi || !thumbnailApi) {
|
if (!mainApi || !thumbnailApi) return;
|
||||||
return;
|
if (isReady) return;
|
||||||
}
|
|
||||||
|
|
||||||
const handleTopSelect = () => {
|
const handleMainSelect = () => {
|
||||||
const selected = mainApi.selectedScrollSnap();
|
const selected = mainApi.selectedScrollSnap();
|
||||||
setCurrent(selected);
|
if (selected !== current) {
|
||||||
thumbnailApi.scrollTo(selected);
|
syncCarousels(selected);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBottomSelect = () => {
|
const handleThumbnailSelect = () => {
|
||||||
const selected = thumbnailApi.selectedScrollSnap();
|
const selected = thumbnailApi.selectedScrollSnap();
|
||||||
setCurrent(selected);
|
if (selected !== current) {
|
||||||
mainApi.scrollTo(selected);
|
syncCarousels(selected);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mainApi.on("select", handleTopSelect);
|
mainApi.on("select", handleMainSelect);
|
||||||
thumbnailApi.on("select", handleBottomSelect);
|
thumbnailApi.on("select", handleThumbnailSelect);
|
||||||
|
|
||||||
|
syncCarousels(0);
|
||||||
|
setIsReady(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mainApi.off("select", handleTopSelect);
|
mainApi.off("select", handleMainSelect);
|
||||||
thumbnailApi.off("select", handleBottomSelect);
|
thumbnailApi.off("select", handleThumbnailSelect);
|
||||||
};
|
};
|
||||||
}, [mainApi, thumbnailApi]);
|
}, [mainApi, thumbnailApi, current, syncCarousels, isReady]);
|
||||||
|
|
||||||
const handleClick = (index: number) => {
|
|
||||||
if (!mainApi || !thumbnailApi) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thumbnailApi.scrollTo(index);
|
|
||||||
mainApi.scrollTo(index);
|
|
||||||
setCurrent(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl sm:w-auto">
|
<div className="w-full max-w-xl sm:w-auto">
|
||||||
<Carousel setApi={setMainApi}>
|
<Carousel setApi={setMainApi} className="mb-2">
|
||||||
<CarouselContent className="m-1">{mainImage}</CarouselContent>
|
<CarouselContent className="m-1">{mainImage}</CarouselContent>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
<Carousel setApi={setThumbnailApi}>
|
<Carousel setApi={setThumbnailApi} className="cursor-pointer">
|
||||||
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
|
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
294
src/components/dataTable.tsx
Normal file
294
src/components/dataTable.tsx
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { ArrowUpDown, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export type ModalProps = {
|
||||||
|
date: Date;
|
||||||
|
amount: number;
|
||||||
|
name: string;
|
||||||
|
investorId?: string;
|
||||||
|
profileURL?: string;
|
||||||
|
logoURL?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const columns: ColumnDef<ModalProps>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||||
|
Name
|
||||||
|
<ArrowUpDown />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
className="bg-transparent hover:bg-transparent text-current"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = row.getValue("profileURL");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar className="h-9 w-9">
|
||||||
|
<AvatarImage src={row.getValue("logoURL")} />
|
||||||
|
<AvatarFallback>{(row.getValue("name") as string).slice(0, 2)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="ml-2">{row.getValue("name")}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "date",
|
||||||
|
header: () => <div className="text-left">Date</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const formatted = new Date(row.getValue("date")).toUTCString();
|
||||||
|
return <div className=" font-medium">{formatted}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span
|
||||||
|
className={`animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75 ${
|
||||||
|
row.getValue("status") === "In Progress"
|
||||||
|
? "bg-sky-400"
|
||||||
|
: row.getValue("status") === "Completed"
|
||||||
|
? "bg-green-400"
|
||||||
|
: "bg-yellow-400"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
className={`relative inline-flex rounded-full h-2 w-2 mt-[2px] ml-0.5 ${
|
||||||
|
row.getValue("status") === "In Progress"
|
||||||
|
? "bg-sky-500"
|
||||||
|
: row.getValue("status") === "Completed"
|
||||||
|
? "bg-green-500"
|
||||||
|
: "bg-yellow-500"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<p
|
||||||
|
className={`text-xs m-0 ${
|
||||||
|
row.getValue("status") === "In Progress"
|
||||||
|
? "text-sky-500"
|
||||||
|
: row.getValue("status") === "Completed"
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-yellow-500"
|
||||||
|
}`}
|
||||||
|
></p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`capitalize ${
|
||||||
|
row.getValue("status") === "In Progress"
|
||||||
|
? "text-sky-500"
|
||||||
|
: row.getValue("status") === "Completed"
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-yellow-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.getValue("status")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "amount",
|
||||||
|
header: () => <div className="text-right">Amount</div>,
|
||||||
|
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 <div className="text-right font-medium">{formatted}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "logoURL",
|
||||||
|
id: "logoURL",
|
||||||
|
header: () => null,
|
||||||
|
cell: () => null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "profileURL",
|
||||||
|
id: "profileURL",
|
||||||
|
header: () => null,
|
||||||
|
cell: () => null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DataTable({ data }: { data: ModalProps[] }) {
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
const [pagination, setPagination] = React.useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-3/4 md:w-full">
|
||||||
|
<div className="flex items-center py-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter names..."
|
||||||
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||||
|
onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="ml-auto">
|
||||||
|
Columns <ChevronDown />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide() && column.id != "logoURL" && column.id != "profileURL")
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead 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) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<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 className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
|
||||||
|
selected.
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/components/mobileMenu.tsx
Normal file
27
src/components/mobileMenu.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
export function MobileMenu() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => setIsVisible((prev) => !prev)}>
|
||||||
|
<Menu />
|
||||||
|
</Button>
|
||||||
|
{isVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "-100%" }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: "-100%" }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="fixed top-0 left-0 w-full bg-gray-800 text-white"
|
||||||
|
>
|
||||||
|
<X className="cursor-pointer" onClick={() => setIsVisible(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/components/modal.tsx
Normal file
29
src/components/modal.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { DataTable } from "./dataTable";
|
||||||
|
|
||||||
|
export type ModalProps = {
|
||||||
|
date: Date;
|
||||||
|
amount: number;
|
||||||
|
name: string;
|
||||||
|
investorId?: string;
|
||||||
|
profileURL?: string;
|
||||||
|
logoURL?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Modal({ data }: { data: ModalProps[] }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>View More</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-screen-md md:max-w-screen-lg ">
|
||||||
|
<DataTable data={data} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -34,9 +34,12 @@ export const AuthenticatedComponents = ({ uid, avatarUrl, notificationCount }: A
|
|||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<Bell className="h-6 w-6 " />
|
<Bell className="h-6 w-6 " />
|
||||||
{notificationCount >= 1 && (
|
{notificationCount >= 1 && (
|
||||||
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full animate-ping">
|
<div>
|
||||||
{notificationCount}
|
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full animate-ping"></span>
|
||||||
</span>
|
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full ">
|
||||||
|
{notificationCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
|||||||
import { getUserId } from "@/lib/supabase/actions/getUserId";
|
import { getUserId } from "@/lib/supabase/actions/getUserId";
|
||||||
import { getUnreadNotificationCountByUserId } from "@/lib/data/notificationQuery";
|
import { getUnreadNotificationCountByUserId } from "@/lib/data/notificationQuery";
|
||||||
|
|
||||||
|
import { MobileMenu } from "../mobileMenu";
|
||||||
|
|
||||||
const ListItem = React.forwardRef<React.ElementRef<"a">, React.ComponentPropsWithoutRef<"a">>(
|
const ListItem = React.forwardRef<React.ElementRef<"a">, React.ComponentPropsWithoutRef<"a">>(
|
||||||
({ className, title, children, ...props }, ref) => {
|
({ className, title, children, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
@ -97,8 +99,10 @@ export async function NavigationBar() {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
<div className="flex items-center">
|
<MobileMenu />
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:flex items-center ">
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
@ -146,7 +150,7 @@ export async function NavigationBar() {
|
|||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
|
|
||||||
<div className="flex gap-2 pl-2">
|
<div className="hidden md:flex gap-2 pl-2">
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -115,14 +115,16 @@ const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
|
|||||||
scrollNext,
|
scrollNext,
|
||||||
canScrollPrev,
|
canScrollPrev,
|
||||||
canScrollNext,
|
canScrollNext,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
role="region"
|
role="region"
|
||||||
aria-roledescription="carousel"
|
aria-roledescription="carousel"
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
@ -183,7 +185,8 @@ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProp
|
|||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -210,7 +213,8 @@ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<ty
|
|||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -20,6 +20,9 @@ export const getInvestmentByProjectsIds = (client: SupabaseClient, projectIds: s
|
|||||||
deal_status:value
|
deal_status:value
|
||||||
),
|
),
|
||||||
project_id,
|
project_id,
|
||||||
|
...project_id (
|
||||||
|
project_name
|
||||||
|
),
|
||||||
deal_amount,
|
deal_amount,
|
||||||
created_time,
|
created_time,
|
||||||
...profiles (
|
...profiles (
|
||||||
@ -56,7 +59,25 @@ export const getInvestmentByUserId = (client: SupabaseClient, userId: string) =>
|
|||||||
export function getInvestorDeal(client: SupabaseClient, userId: string) {
|
export function getInvestorDeal(client: SupabaseClient, userId: string) {
|
||||||
return client
|
return client
|
||||||
.from("investment_deal")
|
.from("investment_deal")
|
||||||
.select("*")
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
...deal_status_id(
|
||||||
|
deal_status:value
|
||||||
|
),
|
||||||
|
project_id,
|
||||||
|
...project_id (
|
||||||
|
project_name
|
||||||
|
),
|
||||||
|
deal_amount,
|
||||||
|
created_time,
|
||||||
|
...profiles (
|
||||||
|
investor_id:id,
|
||||||
|
username,
|
||||||
|
avatar_url
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
.in("investor_id", [userId])
|
.in("investor_id", [userId])
|
||||||
.order("created_time", { ascending: true });
|
.order("created_time", { ascending: true });
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user