Merge branch 'front-end' into back-end

This commit is contained in:
Sosokker 2024-11-10 23:24:00 +07:00
commit d5c6590cd3
9 changed files with 480 additions and 320 deletions

View File

@ -5,13 +5,12 @@ 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 { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress"; 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 { DisplayFullImage } from "./displayImage";
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";
@ -19,6 +18,7 @@ import { redirect } from "next/navigation";
import { isOwnerOfProject } from "./query"; import { isOwnerOfProject } from "./query";
import { UpdateTab } from "./UpdateTab"; import { UpdateTab } from "./UpdateTab";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import Gallery from "@/components/carousel";
const PHOTO_MATERIAL_ID = 2; const PHOTO_MATERIAL_ID = 2;
@ -75,7 +75,6 @@ export default async function ProjectDealPage({ params }: { params: { id: number
? projectMaterial.flatMap((item) => ? projectMaterial.flatMap((item) =>
(item.material_url || ["/boiler1.jpg"]).map((url: string) => ({ (item.material_url || ["/boiler1.jpg"]).map((url: string) => ({
src: url, src: url,
alt: "Image",
})) }))
) )
: [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }]; : [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }];
@ -104,35 +103,8 @@ export default async function ProjectDealPage({ params }: { params: { id: number
</div> </div>
<div id="sub-content" className="flex flex-row mt-5"> <div id="sub-content" className="flex flex-row mt-5">
{/* image carousel */} {/* image carousel */}
<div id="image-carousel" className="shrink-0 w-[700px] flex flex-col"> <div id="image-carousel" className="w-full">
{/* first carousel */} <Gallery images={carouselData} />
<Carousel className="w-full h-[400px] ml-1 overflow-hidden">
<CarouselContent className="flex h-full">
{carouselData.map((item, index) => (
<CarouselItem key={index}>
<Image src={item.src} alt={item.alt} width={700} height={400} className="rounded-lg object-cover" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
<CarouselNext className="absolute right-2 top-1/2 transform -translate-y-1/2 z-10 text-white bg-black opacity-50 hover:opacity-100" />
</Carousel>
{/* second carousel */}
<Carousel className="w-full ml-1 h-[100px] mt-5 overflow-hidden">
<CarouselContent className="flex space-x-1 h-[100px]">
{carouselData.map((item, index) => (
<CarouselItem key={index} className="flex">
<DisplayFullImage
src={item.src}
alt={item.alt}
width={200}
height={100}
className="rounded-lg object-cover h-[100px] w-[200px]"
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div> </div>
<Card className="w-[80%] ml-10 shadow-sm"> <Card className="w-[80%] ml-10 shadow-sm">

View File

@ -1,53 +0,0 @@
import { useEffect, useState } from "react";
import { Deal, getDealList, convertToGraphData, getRecentDealData } from "../api/dealApi";
import { RecentDealData } from "@/components/recent-funds";
import { getCurrentUserID } from "../api/userApi";
// custom hook for deal list
export function useDealList() {
const [dealList, setDealList] = useState<Deal[]>([]);
const fetchDealList = async () => {
// set the state to the deal list of current business user
setDealList(await getDealList(await getCurrentUserID()));
};
useEffect(() => {
fetchDealList();
}, []);
return dealList;
}
export function useGraphData() {
const [graphData, setGraphData] = useState({});
const fetchGraphData = async () => {
// fetch the state to the deal list of current business user
const dealList = await getDealList(await getCurrentUserID());
if (dealList) {
setGraphData(convertToGraphData(dealList));
}
};
useEffect(() => {
fetchGraphData();
}, []);
return graphData;
}
export function useRecentDealData() {
const [recentDealData, setRecentDealData] = useState<RecentDealData[]>();
const fetchRecentDealData = async () => {
// set the state to the deal list of current business user
setRecentDealData(await getRecentDealData(await getCurrentUserID()));
};
useEffect(() => {
fetchRecentDealData();
}, []);
return recentDealData;
}

View File

@ -5,84 +5,104 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Overview } from "@/components/ui/overview"; import { Overview } from "@/components/ui/overview";
import { RecentFunds } from "@/components/recent-funds"; import { RecentFunds } from "@/components/recent-funds";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDealList } from "./hook";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient"; import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import useSession from "@/lib/supabase/useSession"; import useSession from "@/lib/supabase/useSession";
import { getProjectByUserId } from "@/lib/data/projectQuery"; import { getProjectByUserId } from "@/lib/data/projectQuery";
import { Loader } from "@/components/loading/loader"; import { Loader } from "@/components/loading/loader";
import { getInvestmentByProjectsIds } from "@/lib/data/investmentQuery";
const data = [ import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
{ import { overAllGraphData, Deal, fourYearGraphData, dayOftheWeekData } from "../portfolio/[uid]/query";
name: "Jan", import CountUp from "react-countup";
value: Math.floor(Math.random() * 5000) + 1000, import { Button } from "@/components/ui/button";
},
{
name: "Feb",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Mar",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Apr",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "May",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jun",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jul",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Aug",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Sep",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Oct",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Nov",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Dec",
value: Math.floor(Math.random() * 5000) + 1000,
},
];
export default function Dashboard() { export default function Dashboard() {
let supabase = createSupabaseClient(); const supabase = createSupabaseClient();
const userId = useSession().session?.user.id; const userId = useSession().session?.user.id;
const [projects, setProjects] = useState< const [projects, setProjects] = useState<
{ id: number; project_name: string; business_id: { user_id: number }[]; dataroom_id: number }[] { id: number; project_name: string; business_id: { user_id: number }[]; dataroom_id: number }[]
>([]); >([]);
const [latestInvestment, setLatestInvestment] = useState<
{
avatarUrl: string;
createdTime: Date;
dealAmount: number;
dealStatus: string;
investorId: string;
username: string;
}[]
>([]);
const tabOptions = ["daily", "monthly", "yearly"];
const [activeTab, setActiveTab] = useState("daily");
const [isSuccess, setIsSuccess] = useState(false); const [isSuccess, setIsSuccess] = useState(false);
const [graphType, setGraphType] = useState("line"); const [graphType, setGraphType] = useState("line");
const dealList = useDealList(); const [currentProjectId, setCurrentProjectId] = useState<number>(projects[0]?.id);
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
const investmentDetail = useQuery(
getInvestmentByProjectsIds(
supabase,
projects.map((item) => {
return item.id.toString();
})
)
);
let graphData = [];
const filteredData = (investmentDetail?.data || []).filter((deal) => deal.project_id === currentProjectId);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
};
if (activeTab === "daily") {
graphData = dayOftheWeekData(filteredData);
} else if (activeTab === "yearly") {
graphData = fourYearGraphData(filteredData);
} else {
graphData = overAllGraphData(filteredData);
}
useEffect(() => {
const setTopLatestInvestment = () => {
if (investmentDetail?.data) {
setLatestInvestment(
investmentDetail.data
.slice(0, 8)
.map((item) => {
// set the project according to current project id
if (item.project_id === currentProjectId) {
return {
avatarUrl: item.avatar_url,
createdTime: item.created_time,
dealAmount: item.deal_amount,
dealStatus: item.deal_status,
investorId: item.investor_id,
username: item.username,
};
}
return undefined;
})
.filter((item) => item !== undefined) as {
avatarUrl: string;
createdTime: Date;
dealAmount: number;
dealStatus: string;
investorId: string;
username: string;
}[]
);
// console.table(latestInvestment);
}
};
setTopLatestInvestment();
}, [supabase, investmentDetail]);
useEffect(() => { useEffect(() => {
const fetchProjects = async () => { const fetchProjects = async () => {
if (userId) { if (userId) {
const { data, error } = await getProjectByUserId(supabase, userId); const { data, error } = await getProjectByUserId(supabase, userId);
// alert(JSON.stringify(data));
if (error) { if (error) {
console.error("Error while fetching projects"); console.error("Error while fetching projects");
} }
if (data) { if (data) {
// set project and current project id
setProjects(data); setProjects(data);
// console.table(data); setCurrentProjectId(data[0].id);
} }
} else { } else {
console.error("Error with UserId while fetching projects"); console.error("Error with UserId while fetching projects");
@ -93,7 +113,7 @@ export default function Dashboard() {
}, [supabase, userId]); }, [supabase, userId]);
return ( return (
<> <div className="container max-w-screen-xl">
<Loader isSuccess={isSuccess} /> <Loader isSuccess={isSuccess} />
<div className="md:hidden"> <div className="md:hidden">
<Image <Image
@ -116,148 +136,173 @@ export default function Dashboard() {
<div className="flex items-center justify-between space-y-2"> <div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2> <h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
</div> </div>
<Tabs defaultValue={projects[0].project_name} className="space-y-4"> {projects && projects.length > 0 && (
<TabsList> <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) => (
<TabsTrigger key={project.id} value={project.project_name}> <TabsContent value={project.project_name} className="space-y-4">
{project.project_name} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
</TabsTrigger> <Card>
))} <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
</TabsList> <CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle>
<TabsContent value={projects[0].project_name} className="space-y-4"> <svg
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> xmlns="http://www.w3.org/2000/svg"
<Card> viewBox="0 0 24 24"
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> fill="none"
<CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle> stroke="currentColor"
<svg strokeLinecap="round"
xmlns="http://www.w3.org/2000/svg" strokeLinejoin="round"
viewBox="0 0 24 24" strokeWidth="2"
fill="none" className="h-4 w-4 text-muted-foreground"
stroke="currentColor" >
strokeLinecap="round" <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
strokeLinejoin="round" </svg>
strokeWidth="2" </CardHeader>
className="h-4 w-4 text-muted-foreground" <CardContent>
> <div className="text-2xl font-bold">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" /> $
</svg> <CountUp
</CardHeader> end={filteredData
<CardContent> .filter((project) => project.deal_status === "Completed")
<div className="text-2xl font-bold">${totalDealAmount}</div> .reduce((sum, current) => sum + current.deal_amount, 0)}
{/* <p className="text-xs text-muted-foreground"> duration={1}
+20.1% from last month />
</p> */} </div>
</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">Profile Views</CardTitle> <CardTitle className="text-sm font-medium">Profile Views</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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <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> <circle cx="12" cy="12" r="3"></circle>
</svg> </svg>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">+2350</div> <div className="text-2xl font-bold">
{/* <p className="text-xs text-muted-foreground"> +<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">+12,234</div> <div className="text-2xl font-bold">
{/* <p className="text-xs text-muted-foreground"> +<CountUp end={12234} duration={1} />
</div>
{/* <p className="text-xs text-muted-foreground">
+19% from last month +19% from last month
</p> */} </p> */}
</CardContent> </CardContent>
</Card> </Card>
{/* <Card> <Button
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> onClick={() => {
<CardTitle className="text-sm font-medium"> window.location.href = `/project/${project.id}/edit`;
Active Now }}
</CardTitle> className="h-full bg-emerald-500 hover:bg-emerald-800 font-bold text-xl"
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
> >
<path d="M22 12h-4l-3 9L9 3l-3 9H2" /> Edit Project
</svg> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
</CardHeader> <path
<CardContent> 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"
<div className="text-2xl font-bold">+573</div> fill="currentColor"
<p className="text-xs text-muted-foreground"> />
+201 since last hour </svg>
</p> </Button>
</CardContent> </div>
</Card> */} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
</div> <Card className="col-span-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7"> <CardHeader>
<Card className="col-span-4"> <CardTitle>Overview</CardTitle>
<CardHeader> </CardHeader>
<CardTitle>Overview</CardTitle> <Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
</CardHeader> <TabsList className="grid w-56 grid-cols-3 ml-5">
<CardContent className="pl-2"> {tabOptions.map((tab) => (
<Overview graphType={graphType} data={data} /> <TabsTrigger key={tab} value={tab}>
{/* tab to switch between line and bar graph */} {tab.charAt(0).toUpperCase() + tab.slice(1)}
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2"> </TabsTrigger>
<TabsList> ))}
<TabsTrigger value="line" onClick={() => setGraphType("line")}> </TabsList>
Line <CardContent className="pl-2 mt-5">
</TabsTrigger> <Overview graphType={graphType} data={graphData} />
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
Bar <Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
</TabsTrigger> <TabsList>
</TabsList> <TabsTrigger value="line" onClick={() => setGraphType("line")}>
</Tabs> Line
</CardContent> </TabsTrigger>
</Card> <TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
<Card className="col-span-3"> Bar
<CardHeader> </TabsTrigger>
<CardTitle>Recent Funds</CardTitle> </TabsList>
<CardDescription>You made {dealList?.length || 0} sales this month.</CardDescription> </Tabs>
</CardHeader> </CardContent>
<CardContent> </Tabs>
<RecentFunds></RecentFunds> </Card>
</CardContent> <Card className="col-span-3">
</Card> <CardHeader>
</div> <CardTitle>Recent Funds</CardTitle>
</TabsContent> </CardHeader>
</Tabs> <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>
</Card>
</div>
</TabsContent>
))}
</Tabs>
)}
</div> </div>
</div> </div>
</> </div>
); );
} }

View File

@ -52,7 +52,6 @@ export default async function Portfolio({ params }: { params: { uid: string } })
); );
} }
const username = localUser ? localUser.user.user_metadata.name : "Anonymous"; const username = localUser ? localUser.user.user_metadata.name : "Anonymous";
// console.log(username)
const overAllData = deals ? overAllGraphData(deals) : []; const overAllData = deals ? overAllGraphData(deals) : [];
const fourYearData = deals ? fourYearGraphData(deals) : []; const fourYearData = deals ? fourYearGraphData(deals) : [];
const dayOfWeekData = deals ? dayOftheWeekData(deals) : []; const dayOfWeekData = deals ? dayOftheWeekData(deals) : [];
@ -66,31 +65,13 @@ export default async function Portfolio({ params }: { params: { uid: string } })
) )
: []; : [];
const totalInvestment = deals ? getTotalInvestment(deals) : 0; const totalInvestment = deals ? getTotalInvestment(deals) : 0;
// console.log(latestDeals);
const tagCount = countTags(tags); const tagCount = countTags(tags);
// console.log(investedBusinessIds);
const businessType = deals const businessType = deals
? await Promise.all(deals.map(async (item) => await getBusinessTypeName(supabase, item.project_id))) ? await Promise.all(deals.map(async (item) => await getBusinessTypeName(supabase, item.project_id)))
: []; : [];
const countedBusinessType = countValues(businessType.filter((item) => item !== null)); const countedBusinessType = countValues(businessType.filter((item) => item !== null));
// console.log(countedBusinessType);
// console.log(tagCount);
return ( return (
<div className="container max-w-screen-xl"> <div className="container max-w-screen-xl">
{/* {JSON.stringify(params.uid)} */}
{/* {JSON.stringify(tagCount)} */}
{/* {JSON.stringify(deals)} */}
{/* {JSON.stringify(dayOfWeekData)} */}
{/* {JSON.stringify(overAllGraphData)} */}
{/* {JSON.stringify(threeYearGraphData)} */}
{/* {JSON.stringify(uniqueProjectIds)} */}
{/* <div className="flex flex-row">
<h1>Total Invest : </h1>
<div>{totalInvest}</div>
</div> */}
{/* <CountUpComponent end={100} duration={3} /> */}
<div className="text-center py-4"> <div className="text-center py-4">
<h1 className="text-2xl font-semibold">Welcome to your Portfolio, {username}!</h1> <h1 className="text-2xl font-semibold">Welcome to your Portfolio, {username}!</h1>
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">

View File

@ -26,11 +26,11 @@ 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;}[]
) { ) {
const llist = []; const llist = [];
const count = 8; const count = 8;
// select project name from the given id
for (let i = deals.length - 1; i >= 0 && llist.length < count; --i) { for (let i = deals.length - 1; i >= 0 && llist.length < count; --i) {
let { data: project, error } = await supabase.from("project").select("project_name").eq("id", deals[i].project_id); let { data: project, error } = await supabase.from("project").select("project_name").eq("id", deals[i].project_id);
if (error) { if (error) {
@ -38,6 +38,7 @@ async function getLatestInvestment(
} }
let url = fetchLogoURL(supabase, deals[i].project_id); let url = fetchLogoURL(supabase, deals[i].project_id);
llist.push({ llist.push({
projectId: deals[i].project_id,
name: project?.[0]?.project_name, name: project?.[0]?.project_name,
amount: deals[i].deal_amount, amount: deals[i].deal_amount,
date: new Date(deals[i].created_time), date: new Date(deals[i].created_time),
@ -104,7 +105,7 @@ async function getBusinessTypeName(supabase: SupabaseClient, projectId: number)
} }
// only use deal that were made at most year ago // only use deal that were made at most year ago
interface Deal { export interface Deal {
created_time: string | number | Date; created_time: string | number | Date;
deal_amount: any; deal_amount: any;
} }

View File

@ -0,0 +1,88 @@
"use client";
import { useEffect, useState, useMemo } from "react";
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
import Image from "next/image";
interface GalleryProps {
images: { src: string }[];
}
const Gallery = ({ images }: GalleryProps) => {
const [mainApi, setMainApi] = useState<CarouselApi>();
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
const mainImage = useMemo(
() =>
images.map((image, index) => (
<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" }} />
</CarouselItem>
)),
[images]
);
const thumbnailImages = useMemo(
() =>
images.map((image, index) => (
<CarouselItem key={index} className="relative aspect-square basis-1/4" onClick={() => handleClick(index)}>
<Image
className={`${index === current ? "border-2" : ""}`}
src={image.src}
fill
alt={`Carousel Thumbnail Image ${index + 1}`}
style={{ objectFit: "contain" }}
/>
</CarouselItem>
)),
[images, current]
);
useEffect(() => {
if (!mainApi || !thumbnailApi) {
return;
}
const handleTopSelect = () => {
const selected = mainApi.selectedScrollSnap();
setCurrent(selected);
thumbnailApi.scrollTo(selected);
};
const handleBottomSelect = () => {
const selected = thumbnailApi.selectedScrollSnap();
setCurrent(selected);
mainApi.scrollTo(selected);
};
mainApi.on("select", handleTopSelect);
thumbnailApi.on("select", handleBottomSelect);
return () => {
mainApi.off("select", handleTopSelect);
thumbnailApi.off("select", handleBottomSelect);
};
}, [mainApi, thumbnailApi]);
const handleClick = (index: number) => {
if (!mainApi || !thumbnailApi) {
return;
}
thumbnailApi.scrollTo(index);
mainApi.scrollTo(index);
setCurrent(index);
};
return (
<div className="w-full max-w-xl sm:w-auto">
<Carousel setApi={setMainApi}>
<CarouselContent className="m-1">{mainImage}</CarouselContent>
</Carousel>
<Carousel setApi={setThumbnailApi}>
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
</Carousel>
</div>
);
};
export default Gallery;

View File

@ -71,15 +71,19 @@ export function ProjectCard(props: ProjectCardProps) {
<span className="text-xs">{props.location}</span> <span className="text-xs">{props.location}</span>
</div> </div>
<div className="flex flex-wrap mt-1 items-center text-muted-foreground"> <div className="flex flex-wrap mt-1 items-center text-muted-foreground">
{props.tags && Array.isArray(props.tags) ? ( {props.tags?.length !== 0 && Array.isArray(props.tags)
props.tags.map((tag) => ( ? props.tags
<span id="tag" key={tag} className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1"> .filter((tag) => tag && tag.trim() !== "") // Filters out null or blank tags
{tag} .map((tag) => (
</span> <span
)) id="tag"
) : ( key={tag}
<span className="text-xs text-muted-foreground">No tags available</span> className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1"
)} >
{tag}
</span>
))
: null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Link from "next/link";
export type RecentDealData = { export type RecentDealData = {
created_time: Date; created_time: Date;
@ -6,29 +7,127 @@ export type RecentDealData = {
investor_id: string; investor_id: string;
username: string; username: string;
logo_url?: string; logo_url?: string;
status?: string;
// email: string; // email: string;
}; };
interface RecentFundsProps { interface RecentFundsProps {
data?: { name?: string; amount?: number; avatar?: string; date?: Date; logo_url?: string }[]; data?: { name?: string; amount?: number; avatar?: string; date?: Date; logo_url?: string; status?: string; profile_url?: string }[];
} }
export function RecentFunds(props: RecentFundsProps) { export function RecentFunds(props: RecentFundsProps) {
const content = (
<div>
</div>
)
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{(props?.data || []).map((deal, index) => ( {(props?.data || []).map((deal, index) => (
<div className="flex items-center" key={index}> <div key={index}>
<Avatar className="h-9 w-9"> {deal.profile_url ? (
<AvatarImage src={deal.logo_url} alt={deal.name} /> <Link
<AvatarFallback>{(deal.name ?? "").slice(0, 2)}</AvatarFallback> href={deal.profile_url}
</Avatar> className="flex items-center w-full"
<div className="ml-4 space-y-1"> target="_blank"
<p className="text-sm font-medium leading-none">{deal.name}</p> rel="noopener noreferrer"
<p className="text-xs text-muted-foreground">{deal?.date?.toLocaleDateString()}</p> >
</div> <Avatar className="h-9 w-9">
<div className="ml-auto font-medium">+${deal.amount}</div> <AvatarImage src={deal.logo_url ? deal.logo_url : deal.avatar} alt={deal.name} />
<AvatarFallback>{(deal.name ?? "").slice(0, 2)}</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{deal.name}</p>
<p className="text-xs text-muted-foreground">{deal?.date?.toLocaleDateString()}</p>
{deal.status && (
<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 ${
deal?.status === "In Progress"
? "bg-sky-400"
: deal?.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 ${
deal?.status === "In Progress"
? "bg-sky-500"
: deal?.status === "Completed"
? "bg-green-500"
: "bg-yellow-500"
}`}
></span>
</span>
<p
className={`text-xs m-0 ${
deal?.status === "In Progress"
? "text-sky-500"
: deal?.status === "Completed"
? "text-green-500"
: "text-yellow-500"
}`}
>
{deal?.status}
</p>
</div>
)}
</div>
<div className="ml-auto font-medium">+${deal.amount}</div>
</Link>
) : (
<div className="flex items-center w-full">
<Avatar className="h-9 w-9">
<AvatarImage src={deal.logo_url ? deal.logo_url : deal.avatar} alt={deal.name} />
<AvatarFallback>{(deal.name ?? "").slice(0, 2)}</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{deal.name}</p>
<p className="text-xs text-muted-foreground">{deal?.date?.toLocaleDateString()}</p>
{deal.status && (
<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 ${
deal?.status === "In Progress"
? "bg-sky-400"
: deal?.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 ${
deal?.status === "In Progress"
? "bg-sky-500"
: deal?.status === "Completed"
? "bg-green-500"
: "bg-yellow-500"
}`}
></span>
</span>
<p
className={`text-xs m-0 ${
deal?.status === "In Progress"
? "text-sky-500"
: deal?.status === "Completed"
? "text-green-500"
: "text-yellow-500"
}`}
>
{deal?.status}
</p>
</div>
)}
</div>
<div className="ml-auto font-medium">+${deal.amount}</div>
</div>
)}
</div> </div>
))} ))}
</div> </div>
); );
} }

View File

@ -10,6 +10,29 @@ export const getInvestmentCountsByProjectsIds = (client: SupabaseClient, project
.in("project_id", projectIds); .in("project_id", projectIds);
}; };
export const getInvestmentByProjectsIds = (client: SupabaseClient, projectIds: string[]) => {
return client
.from("investment_deal")
.select(
`
id,
...deal_status_id(
deal_status:value
),
project_id,
deal_amount,
created_time,
...profiles (
investor_id:id,
username,
avatar_url
)
`
)
.in("project_id", projectIds)
.order("created_time", { ascending: false });
};
export const getInvestmentByUserId = (client: SupabaseClient, userId: string) => { export const getInvestmentByUserId = (client: SupabaseClient, userId: string) => {
return client return client
.from("investment_deal") .from("investment_deal")