mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-19 05:54:06 +01:00
Merge branch 'back-end' of https://github.com/Sosokker/B2D-Ventures into back-end
This commit is contained in:
commit
e29eeab868
@ -13,24 +13,34 @@ 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 { sumByKey, toPercentage } from "@/lib/utils";
|
||||||
|
|
||||||
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
|
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
|
|
||||||
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
|
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
|
||||||
|
|
||||||
const carouselData = [
|
if (!projectData) {
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
return <div>No data available</div>;
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
}
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (projectDataError) {
|
if (projectDataError) {
|
||||||
return <div>Error</div>;
|
return <div>Error</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectBusinessOwnerId = projectData.user_id;
|
||||||
|
const dealList = await getDealList(projectBusinessOwnerId);
|
||||||
|
const totalDealAmount = sumByKey(dealList, "deal_amount");
|
||||||
|
// timeDiff, if negative convert to zero
|
||||||
|
const timeDiff = Math.max((new Date(projectData.investment_deadline)).getTime() - new Date().getTime(), 0)
|
||||||
|
const hourLeft = Math.floor(timeDiff / (1000 * 60 * 60));
|
||||||
|
|
||||||
|
const carouselData = Array(5).fill({
|
||||||
|
src: projectData.card_image_url || "/boiler1.jpg",
|
||||||
|
alt: `${projectData.project_name} Image`,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl my-5">
|
<div className="container max-w-screen-xl my-5">
|
||||||
<div className="flex flex-col gap-y-10">
|
<div className="flex flex-col gap-y-10">
|
||||||
@ -82,24 +92,33 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
|
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
|
||||||
<div className="pl-5">
|
<div className="pl-5">
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8">${projectData?.total_investment}</h1>
|
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
|
||||||
<p className="text-sm md:text-lg"> 5% raised of \$5M max goal</p>
|
<p className="text-sm md:text-lg">
|
||||||
|
{toPercentage(totalDealAmount, projectData?.target_investment)}%
|
||||||
|
raised of ${projectData?.target_investment} max goal
|
||||||
|
</p>
|
||||||
<Progress
|
<Progress
|
||||||
value={projectData?.total_investment / projectData?.target_investment}
|
value={toPercentage(totalDealAmount, projectData?.target_investment)}
|
||||||
className="w-[60%] h-3 mt-3"
|
className="w-[60%] h-3 mt-3"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-4xl md:mt-8">
|
<h1 className="font-semibold text-4xl md:mt-8">
|
||||||
<p className="text-xl md:text-4xl">{projectData?.total_investment}</p>
|
<p className="text-xl md:text-4xl">{dealList.length}</p>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm md:text-lg"> Investors</p>
|
<p className="text-sm md:text-lg">Investors</p>
|
||||||
</span>
|
</span>
|
||||||
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
|
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
|
||||||
<p className="text-xl md:text-4xl">1 hours</p>
|
{projectData?.investment_deadline ? (
|
||||||
<p> Left to invest</p>
|
<>
|
||||||
|
<p className="text-xl md:text-4xl">{Math.floor(hourLeft)} hours</p>
|
||||||
|
<p>Left to invest</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xl md:text-4xl">No deadline</p>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Button className="mt-5 w-3/4 h-12">
|
<Button className="mt-5 w-3/4 h-12">
|
||||||
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
|
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
|
||||||
@ -121,8 +140,8 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
<Tabs.Content value="pitch">
|
<Tabs.Content value="pitch">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle></CardTitle>
|
<CardTitle>{projectData.project_name}</CardTitle>
|
||||||
<CardDescription></CardDescription>
|
<CardDescription />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="prose prose-sm max-w-none ">
|
<div className="prose prose-sm max-w-none ">
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { getCurrentUserID } from "./userApi";
|
|
||||||
|
|
||||||
export type Deal = {
|
export type Deal = {
|
||||||
deal_amount: number;
|
deal_amount: number;
|
||||||
@ -7,9 +6,14 @@ export type Deal = {
|
|||||||
investor_id: string;
|
investor_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getDealList() {
|
export async function getDealList(userId: string | undefined) {
|
||||||
|
if (!userId) {
|
||||||
|
// console.error("No deal list of this user was found");
|
||||||
|
return []; // Exit on error
|
||||||
|
}
|
||||||
|
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
// get id of investor who invests in the business
|
// get id of investors who invest in the business
|
||||||
const { data: dealData, error: dealError } = await supabase
|
const { data: dealData, error: dealError } = await supabase
|
||||||
.from("business")
|
.from("business")
|
||||||
.select(
|
.select(
|
||||||
@ -21,18 +25,18 @@ export async function getDealList() {
|
|||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.eq("user_id", await getCurrentUserID())
|
.eq("user_id", userId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (dealError) {
|
if (dealError) {
|
||||||
alert(JSON.stringify(dealError));
|
// alert(JSON.stringify(dealError));
|
||||||
console.error("Error fetching deal list:", dealError);
|
console.error("Error fetching deal list:", dealError);
|
||||||
return; // Exit on error
|
return []; // Exit on error
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dealData || !dealData.project.length) {
|
if (!dealData || !dealData.project.length) {
|
||||||
alert("No project available");
|
alert("No project available");
|
||||||
return; // Exit if there's no data
|
return []; // Exit if there's no data
|
||||||
}
|
}
|
||||||
|
|
||||||
const investorIdList = dealData.project[0].investment_deal.map((deal) => deal.investor_id);
|
const investorIdList = dealData.project[0].investment_deal.map((deal) => deal.investor_id);
|
||||||
@ -53,16 +57,22 @@ export async function getDealList() {
|
|||||||
if (sortedDealDataError) {
|
if (sortedDealDataError) {
|
||||||
alert(JSON.stringify(sortedDealDataError));
|
alert(JSON.stringify(sortedDealDataError));
|
||||||
console.error("Error sorting deal list:", sortedDealDataError);
|
console.error("Error sorting deal list:", sortedDealDataError);
|
||||||
return; // Exit on error
|
return []; // Exit on error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(sortedDealData)
|
||||||
return sortedDealData;
|
return sortedDealData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #TODO fix query to be non unique
|
// #TODO fix query to be non unique
|
||||||
export async function getRecentDealData() {
|
export async function getRecentDealData(userId: string | undefined) {
|
||||||
|
if (!userId) {
|
||||||
|
console.error("User not found");
|
||||||
|
return; // Exit on error
|
||||||
|
}
|
||||||
|
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
const dealList = await getDealList();
|
const dealList = await getDealList(userId);
|
||||||
|
|
||||||
if (!dealList) {
|
if (!dealList) {
|
||||||
// #TODO div error
|
// #TODO div error
|
||||||
@ -100,6 +110,8 @@ export async function getRecentDealData() {
|
|||||||
const recentDealData = recentDealList.map((item, index) => {
|
const recentDealData = recentDealList.map((item, index) => {
|
||||||
return { ...item, ...recentUserData[index] };
|
return { ...item, ...recentUserData[index] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return recentDealData;
|
return recentDealData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,8 +133,6 @@ export function convertToGraphData(deals: Deal[]): Record<string, number> {
|
|||||||
|
|
||||||
// Create a sorted graph data object
|
// Create a sorted graph data object
|
||||||
const sortedGraphData: Record<string, number> = {};
|
const sortedGraphData: Record<string, number> = {};
|
||||||
sortedKeys.forEach((key) => {
|
sortedKeys.forEach((key) => {sortedGraphData[key] = graphData[key]});
|
||||||
sortedGraphData[key] = graphData[key];
|
|
||||||
});
|
|
||||||
return sortedGraphData;
|
return sortedGraphData;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Deal, getDealList, convertToGraphData, getRecentDealData } from "../api/dealApi";
|
import { Deal, getDealList, convertToGraphData, getRecentDealData } from "../api/dealApi";
|
||||||
import { RecentDealData } from "@/components/recent-funds";
|
import { RecentDealData } from "@/components/recent-funds";
|
||||||
|
import { getCurrentUserID } from "../api/userApi";
|
||||||
|
|
||||||
// custom hook for deal list
|
// custom hook for deal list
|
||||||
export function useDealList() {
|
export function useDealList() {
|
||||||
const [dealList, setDealList] = useState<Deal[]>();
|
const [dealList, setDealList] = useState<Deal[]>([]);
|
||||||
|
|
||||||
const fetchDealList = async () => {
|
const fetchDealList = async () => {
|
||||||
setDealList(await getDealList());
|
// set the state to the deal list of current business user
|
||||||
|
setDealList(await getDealList(await getCurrentUserID()));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,7 +23,8 @@ export function useGraphData() {
|
|||||||
const [graphData, setGraphData] = useState({});
|
const [graphData, setGraphData] = useState({});
|
||||||
|
|
||||||
const fetchGraphData = async () => {
|
const fetchGraphData = async () => {
|
||||||
const dealList = await getDealList();
|
// fetch the state to the deal list of current business user
|
||||||
|
const dealList = await getDealList(await getCurrentUserID());
|
||||||
if (dealList) {
|
if (dealList) {
|
||||||
setGraphData(convertToGraphData(dealList));
|
setGraphData(convertToGraphData(dealList));
|
||||||
}
|
}
|
||||||
@ -38,7 +41,8 @@ export function useRecentDealData() {
|
|||||||
const [recentDealData, setRecentDealData] = useState<RecentDealData[]>();
|
const [recentDealData, setRecentDealData] = useState<RecentDealData[]>();
|
||||||
|
|
||||||
const fetchRecentDealData = async () => {
|
const fetchRecentDealData = async () => {
|
||||||
setRecentDealData(await getRecentDealData());
|
// set the state to the deal list of current business user
|
||||||
|
setRecentDealData(await getRecentDealData(await getCurrentUserID()));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { RecentFunds } from "@/components/recent-funds";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useDealList, useGraphData, useRecentDealData } from "./hook";
|
import { useDealList, useGraphData, useRecentDealData } from "./hook";
|
||||||
|
import { sumByKey } from "@/lib/utils";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [graphType, setGraphType] = useState("line");
|
const [graphType, setGraphType] = useState("line");
|
||||||
@ -20,17 +21,9 @@ export default function Dashboard() {
|
|||||||
const dealList = useDealList();
|
const dealList = useDealList();
|
||||||
// #TODO dependency injection refactor + define default value inside function (and not here)
|
// #TODO dependency injection refactor + define default value inside function (and not here)
|
||||||
const recentDealData = useRecentDealData() || [];
|
const recentDealData = useRecentDealData() || [];
|
||||||
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* {dealList?.map((deal, index) => (
|
|
||||||
<div key={index} className="deal-item">
|
|
||||||
<p>Deal Amount: {deal.deal_amount}</p>
|
|
||||||
<p>Created Time: {new Date(deal.created_time).toUTCString()}</p>
|
|
||||||
<p>Investor ID: {deal.investor_id}</p>
|
|
||||||
</div>
|
|
||||||
))} */}
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Image
|
<Image
|
||||||
src="/examples/dashboard-light.png"
|
src="/examples/dashboard-light.png"
|
||||||
@ -78,7 +71,7 @@ export default function Dashboard() {
|
|||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">${totalDealAmount}</div>
|
<div className="text-2xl font-bold">${sumByKey(dealList, "deal_amount")}</div>
|
||||||
{/* <p className="text-xs text-muted-foreground">
|
{/* <p className="text-xs text-muted-foreground">
|
||||||
+20.1% from last month
|
+20.1% from last month
|
||||||
</p> */}
|
</p> */}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const BUCKET_PITCH_APPLICATION_NAME = "project-application";
|
|||||||
export default function ApplyProject() {
|
export default function ApplyProject() {
|
||||||
const [isSuccess, setIsSuccess] = useState(true);
|
const [isSuccess, setIsSuccess] = useState(true);
|
||||||
const onSubmit: SubmitHandler<projectSchema> = async (data) => {
|
const onSubmit: SubmitHandler<projectSchema> = async (data) => {
|
||||||
alert("มาแน้ววว");
|
// alert("มาแน้ววว");
|
||||||
await sendApplication(data);
|
await sendApplication(data);
|
||||||
// console.table(data);
|
// console.table(data);
|
||||||
// console.log(typeof data["projectPhotos"], data["projectPhotos"]);
|
// console.log(typeof data["projectPhotos"], data["projectPhotos"]);
|
||||||
|
|||||||
@ -6,28 +6,28 @@ async function getTopProjects(client: SupabaseClient, numberOfRecords: number =
|
|||||||
.from("project")
|
.from("project")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,
|
id,
|
||||||
project_name,
|
project_name,
|
||||||
business_id,
|
business_id,
|
||||||
published_time,
|
published_time,
|
||||||
project_short_description,
|
project_short_description,
|
||||||
card_image_url,
|
card_image_url,
|
||||||
project_investment_detail (
|
project_investment_detail (
|
||||||
min_investment,
|
min_investment,
|
||||||
total_investment,
|
total_investment,
|
||||||
target_investment,
|
target_investment,
|
||||||
investment_deadline
|
investment_deadline
|
||||||
),
|
),
|
||||||
project_tag (
|
project_tag (
|
||||||
tag (
|
tag (
|
||||||
id,
|
id,
|
||||||
value
|
value
|
||||||
)
|
|
||||||
),
|
|
||||||
business (
|
|
||||||
location
|
|
||||||
)
|
)
|
||||||
`
|
),
|
||||||
|
business (
|
||||||
|
location
|
||||||
|
)
|
||||||
|
`
|
||||||
)
|
)
|
||||||
.order("published_time", { ascending: false })
|
.order("published_time", { ascending: false })
|
||||||
.limit(numberOfRecords);
|
.limit(numberOfRecords);
|
||||||
@ -79,6 +79,7 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
|||||||
project_short_description,
|
project_short_description,
|
||||||
project_description,
|
project_description,
|
||||||
published_time,
|
published_time,
|
||||||
|
card_image_url,
|
||||||
...project_investment_detail!inner (
|
...project_investment_detail!inner (
|
||||||
min_investment,
|
min_investment,
|
||||||
total_investment,
|
total_investment,
|
||||||
@ -89,6 +90,9 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
|||||||
...tag!inner (
|
...tag!inner (
|
||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
...business (
|
||||||
|
user_id
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,3 +4,30 @@ import { twMerge } from "tailwind-merge"
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sum(list: any[]) {
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.reduce((total, num) => total + num, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sumByKey(list: any[], key: string) {
|
||||||
|
// example usage
|
||||||
|
// const items = [
|
||||||
|
// { amount: 10 },
|
||||||
|
// { amount: 20 },
|
||||||
|
// { amount: 30 },
|
||||||
|
// { amount: 40 }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// const totalAmount = sumByKey(items, 'amount');
|
||||||
|
// console.log(totalAmount); // Output: 100
|
||||||
|
return list.reduce((total, obj) => total + (obj[key] || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPercentage(part: number, total: number): number {
|
||||||
|
if (total === 0) return 0; // Prevent division by zero
|
||||||
|
return Number(((part * 100) / total).toFixed(2));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user