Merge branch 'front-end' into back-end

This commit is contained in:
Sosokker 2024-11-07 03:26:15 +07:00
commit 9db518cb6b
35 changed files with 20919 additions and 440 deletions

View File

@ -15,6 +15,11 @@ const nextConfig = {
hostname: "upload.wikimedia.org",
pathname: "/wikipedia/**",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
pathname: "/**",
}
],
},
};

32
package-lock.json generated
View File

@ -35,6 +35,7 @@
"@tanstack/react-query": "^5.59.0",
"@tanstack/react-query-devtools": "^5.59.0",
"b2d-ventures": "file:",
"chart.js": "^4.4.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
@ -44,7 +45,8 @@
"lucide-react": "^0.428.0",
"next": "^14.2.15",
"next-themes": "^0.3.0",
"react": "^18",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-countup": "^6.5.3",
"react-day-picker": "^9",
"react-dom": "^18",
@ -68,7 +70,7 @@
"@types/eslint__js": "^8.42.3",
"@types/next": "^8.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.3.12",
"@types/react-dom": "^18",
"@types/react-fade-in": "^2.0.2",
"@types/react-file-icon": "^1.0.4",
@ -1132,6 +1134,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@lexical/clipboard": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.17.1.tgz",
@ -4724,6 +4731,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chart.js": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -10143,6 +10161,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
"integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-countup": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz",
@ -10260,6 +10287,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/react-lottie/-/react-lottie-1.2.4.tgz",
"integrity": "sha512-kBGxI+MIZGBf4wZhNCWwHkMcVP+kbpmrLWH/SkO0qCKc7D7eSPcxQbfpsmsCo8v2KCBYjuGSou+xTqK44D/jMg==",
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.26.0",
"lottie-web": "^5.1.3"

View File

@ -37,6 +37,7 @@
"@tanstack/react-query": "^5.59.0",
"@tanstack/react-query-devtools": "^5.59.0",
"b2d-ventures": "file:",
"chart.js": "^4.4.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
@ -46,7 +47,8 @@
"lucide-react": "^0.428.0",
"next": "^14.2.15",
"next-themes": "^0.3.0",
"react": "^18",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-countup": "^6.5.3",
"react-day-picker": "^9",
"react-dom": "^18",
@ -70,7 +72,7 @@
"@types/eslint__js": "^8.42.3",
"@types/next": "^8.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.3.12",
"@types/react-dom": "^18",
"@types/react-fade-in": "^2.0.2",
"@types/react-file-icon": "^1.0.4",

View File

@ -0,0 +1,42 @@
"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} />;
}

View File

@ -10,8 +10,6 @@ import useSession from "@/lib/supabase/useSession";
import toast from "react-hot-toast";
const FollowShareButtons = () => {
const [progress, setProgress] = useState(0);
const [tab, setTab] = useState("Pitch");
const { session, loading } = useSession();
const user = session?.user;
const [sessionLoaded, setSessionLoaded] = useState(false);

View File

@ -11,17 +11,26 @@ import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import FollowShareButtons from "./followShareButton";
import { DisplayFullImage } from "./displayImage";
import { getProjectData } from "@/lib/data/projectQuery";
import { getDealList } from "@/app/api/dealApi";
import { sumByKey, toPercentage } from "@/lib/utils";
import { redirect } from "next/navigation";
const PHOTO_MATERIAL_ID = 2;
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
const supabase = createSupabaseClient();
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
const { data: projectMaterial, error: projectMaterialError } = await supabase
.from("project_material")
.select("material_url")
.eq("project_id", params.id)
.eq("material_type_id", PHOTO_MATERIAL_ID);
// console.log(projectMaterial);
if (projectMaterialError) {
console.error("Error while fetching project material" + projectMaterialError);
}
if (!projectData) {
redirect("/deals");
}
@ -44,94 +53,107 @@ export default async function ProjectDealPage({ params }: { params: { id: number
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`,
});
const carouselData =
projectMaterial && projectMaterial.length > 0
? projectMaterial.flatMap((item) =>
(item.material_url || ["/boiler1.jpg"]).map((url: string) => ({
src: url,
alt: "Image",
}))
)
: [{ src: "/boiler1.jpg", alt: "Default Boiler Image" }];
// console.log(carouselData);
return (
<div className="container max-w-screen-xl my-5">
<div className="flex flex-col gap-y-10">
<div id="content">
{/* Name, star and share button packed */}
<div id="header" className="flex flex-col">
<div className="flex justify-between">
<span className="flex">
<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>
</span>
<FollowShareButtons />
</div>
{/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
<div className="flex flex-wrap mt-3">
{projectData?.tags.map((tag, index) => (
<span key={index} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag.tag_name}
</span>
))}
</div>
{/* Name, star and share button packed */}
<div id="header" className="flex flex-col">
<div className="flex justify-between">
<span className="flex">
<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>
</span>
<FollowShareButtons />
</div>
{/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
<div className="flex flex-wrap mt-3">
{projectData?.tags.map((tag, index) => (
<span key={index} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
{tag.tag_name}
</span>
))}
</div>
</div>
<div id="sub-content" className="flex flex-row mt-5">
{/* image carousel */}
<div id="image-carousel" className="shrink-0 w-[700px] flex flex-col">
{/* first carousel */}
<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 id="sub-content" className="flex flex-row mt-5">
{/* image carousel */}
<div id="image-corousel" className="shrink-0 w-[700px] flex flex-col">
<Carousel className="w-full h-full ml-1">
<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" />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<Carousel className="w-full ml-1 h-[100px]">
<CarouselContent className="flex space-x-1">
{carouselData.map((item, index) => (
<CarouselItem key={index} className="flex">
<Image src={item.src} alt={item.alt} width={200} height={100} className="rounded-lg basis-0" />
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
<div className="pl-5">
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
<p className="text-sm md:text-lg">
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
{projectData?.target_investment} max goal
</p>
<Progress
value={toPercentage(totalDealAmount, projectData?.target_investment)}
className="w-[60%] h-3 mt-3"
/>
</span>
<span>
<h1 className="font-semibold text-4xl md:mt-8">
<p className="text-xl md:text-4xl">{dealList.length}</p>
</h1>
<p className="text-sm md:text-lg">Investors</p>
</span>
<Separator decorative className="mt-3 w-3/4 ml-5" />
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
{projectData?.investment_deadline ? (
<>
<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>
<Button className="mt-5 w-3/4 h-12">
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
</Button>
</div>
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
<div className="pl-5">
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
<p className="text-sm md:text-lg">
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
{projectData?.target_investment} max goal
</p>
<Progress
value={toPercentage(totalDealAmount, projectData?.target_investment)}
className="w-[60%] h-3 mt-3"
/>
</span>
<span>
<h1 className="font-semibold text-4xl md:mt-8">
<p className="text-xl md:text-4xl">{dealList.length}</p>
</h1>
<p className="text-sm md:text-lg">Investors</p>
</span>
<Separator decorative className="mt-3 w-3/4 ml-5" />
<span>
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
{projectData?.investment_deadline ? (
<>
<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>
<Button className="mt-5 w-3/4 h-12">
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
</Button>
</div>
</div>
</div>

79
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,79 @@
import Image from "next/image";
export default function About() {
// Static data for the cards
const cardData = [
{
imageSrc: 'https://avatars.githubusercontent.com/u/86756025',
name: 'Pattadon Loyprasert',
description:
'Driven by a passion for innovation, Pattadon leads B2D Ventures with the belief that' +
'every great idea deserves the resources and support to thrive. Hes dedicated to' +
'helping entrepreneurs turn visions into reality.',
},
{
imageSrc: 'https://avatars.githubusercontent.com/u/22256420',
name: 'Sirin Puenggun',
description:
'Sirin brings a wealth of experience in empowering entrepreneurs, aiming to' +
'create an ecosystem where bold ideas meet the right partners. Hes committed to' +
'making a lasting impact on the entrepreneurial world.',
},
{
imageSrc: 'https://avatars.githubusercontent.com/u/108450436',
name: 'Naytitorn Chaovirachot',
description:
'With a strong foundation in collaboration and trust, Naytitorn is focused' +
'on building lasting partnerships that help drive the success of both investors and founders.' +
'He thrives on turning challenges into growth opportunities.',
},
{
imageSrc: 'https://avatars.githubusercontent.com/u/114897362',
name: 'Nantawat Sukrisunt',
description:
'Nantawat is a passionate advocate for innovation and teamwork.' +
'He strives to foster a community where both investors and startups can achieve' +
'their full potential, creating a future where collaboration leads to success.',
},
];
return (
<div className="p-10">
<h1 className="mt-3 font-bold text-lg md:text-3xl">About us</h1>
<p className="p-5">
Welcome to B2D Ventures! We&apos;re a dynamic platform committed to bridging the gap
between visionary entrepreneurs and passionate investors. Our mission is to empower
innovation by connecting groundbreaking ideas with the resources they need to thrive.
Through B2D Ventures, we foster a community where investors and innovators come together
to transform concepts into impactful, real-world solutions.
</p>
<p className="p-5">
At B2D Ventures, we believe in the power of collaboration. Whether you&apos;re an investor
looking to support the next big idea or a founder aiming to bring your vision to life,
our platform offers the tools and connections to make it happen.
Join us on a journey to reshape industries, drive positive change, and make a lasting impact.
</p>
<p className="p-5">
Let&apos;s build the future, together.
</p>
<div className="mt-10 text-center">
<h2 className="font-bold text-lg md:text-3xl">Our Team</h2>
</div>
{/* Card Section */}
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{cardData.map((card, index) => (
<div key={index} className="bg-white rounded-lg shadow-lg overflow-hidden">
<Image src={card.imageSrc} width={460} height={460} alt={card.name} className="w-full h-48 object-cover" />
<div className="p-4">
<h3 className="pt-5 text-xl font-semibold text-gray-800 text-center">{card.name}</h3>
<p className="pt-5 text-gray-600 mt-2">{card.description}</p>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -35,7 +35,7 @@ export async function getDealList(userId: string | undefined) {
}
if (!dealData || !dealData.project.length) {
alert("No project available");
// alert("No project available");
return []; // Exit if there's no data
}
@ -70,7 +70,7 @@ export async function getRecentDealData(userId: string | undefined) {
console.error("User not found");
return; // Exit on error
}
const supabase = createSupabaseClient();
const dealList = await getDealList(userId);
@ -111,7 +111,6 @@ export async function getRecentDealData(userId: string | undefined) {
return { ...item, ...recentUserData[index] };
});
return recentDealData;
}
@ -133,6 +132,8 @@ export function convertToGraphData(deals: Deal[]): Record<string, number> {
// Create a sorted graph data object
const sortedGraphData: Record<string, number> = {};
sortedKeys.forEach((key) => {sortedGraphData[key] = graphData[key]});
sortedKeys.forEach((key) => {
sortedGraphData[key] = graphData[key];
});
return sortedGraphData;
}

View File

@ -15,7 +15,6 @@ const BUCKET_PITCH_NAME = "business-application";
let supabase = createSupabaseClient();
export default function ApplyBusiness() {
const [applyProject, setApplyProject] = useState(false);
const alertShownRef = useRef(false);
const [success, setSucess] = useState(false);
@ -49,7 +48,7 @@ export default function ApplyBusiness() {
}
}
const { error } = await supabase
const { data, error } = await supabase
.from("business_application")
.insert([
{
@ -67,28 +66,29 @@ export default function ApplyBusiness() {
])
.select();
setSucess(true);
// console.table(data);
Swal.fire({
icon: error == null ? "success" : "error",
title: error == null ? "success" : "Error: " + error.code,
text: error == null ? "Your application has been submitted" : error.message,
confirmButtonColor: error == null ? "green" : "red",
}).then((result) => {
if (result.isConfirmed && applyProject) {
window.location.href = "/project/apply";
} else {
window.location.href = "/";
}
}).then(() => {
window.location.href = "/";
});
};
const hasUserApplied = async (userID: string) => {
let { data: business, error } = await supabase.from("business").select("*").eq("user_id", userID);
console.table(business);
if (error) {
let { data: businessApplication, error: applicationError } = await supabase
.from("business_application")
.select("*")
.eq("user_id", userID);
// console.table(business);
if (error || applicationError) {
console.error(error);
console.error(applicationError);
}
if (business) {
if ((business && business.length != 0) || (businessApplication && businessApplication.length != 0)) {
return true;
}
return false;
@ -165,7 +165,7 @@ export default function ApplyBusiness() {
</div>
{/* form */}
{/* <form action="" onSubmit={handleSubmit(handleSubmitForms)}> */}
<BusinessForm onSubmit={onSubmit} applyProject={applyProject} setApplyProject={setApplyProject} />
<BusinessForm onSubmit={onSubmit} />
</div>
);
}

View File

@ -10,7 +10,7 @@ export function useDealList() {
const fetchDealList = async () => {
// set the state to the deal list of current business user
setDealList(await getDealList(await getCurrentUserID()));
}
};
useEffect(() => {
fetchDealList();
@ -28,7 +28,7 @@ export function useGraphData() {
if (dealList) {
setGraphData(convertToGraphData(dealList));
}
}
};
useEffect(() => {
fetchGraphData();
@ -43,11 +43,11 @@ export function useRecentDealData() {
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

@ -1,29 +1,101 @@
"use client";
import Image from "next/image";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Overview } from "@/components/ui/overview";
import { RecentFunds } from "@/components/recent-funds";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useDealList } from "./hook";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import useSession from "@/lib/supabase/useSession";
import { getProjectByUserId } from "@/lib/data/projectQuery";
import { Loader } from "@/components/loading/loader";
import { useDealList, useGraphData, useRecentDealData } from "./hook";
import { sumByKey } from "@/lib/utils";
const data = [
{
name: "Jan",
value: Math.floor(Math.random() * 5000) + 1000,
},
{
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() {
let supabase = createSupabaseClient();
const userId = useSession().session?.user.id;
const [projects, setProjects] = useState<
{ id: number; project_name: string; business_id: { user_id: number }[]; dataroom_id: number }[]
>([]);
const [isSuccess, setIsSuccess] = useState(false);
const [graphType, setGraphType] = useState("line");
const graphData = useGraphData();
const [currentTab, setCurrentTab] = useState();
const dealList = useDealList();
// #TODO dependency injection refactor + define default value inside function (and not here)
const recentDealData = useRecentDealData() || [];
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
useEffect(() => {
const fetchProjects = async () => {
if (userId) {
const { data, error } = await getProjectByUserId(supabase, userId);
// alert(JSON.stringify(data));
if (error) {
console.error("Error while fetching projects");
}
if (data) {
setProjects(data);
// console.table(data);
}
} else {
console.error("Error with UserId while fetching projects");
}
setIsSuccess(true);
};
fetchProjects();
}, [supabase, userId]);
return (
<>
<Loader isSuccess={isSuccess} />
<div className="md:hidden">
<Image
src="/examples/dashboard-light.png"
@ -45,18 +117,17 @@ export default function Dashboard() {
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<Tabs defaultValue={projects[0].project_name} className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
{projects.map((project) => (
<TabsTrigger value={project.project_name}>{project.project_name}</TabsTrigger>
))}
</TabsList>
<TabsContent value="overview" className="space-y-4">
<TabsContent value={projects[0].project_name} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Funds Raised
</CardTitle>
<CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@ -71,7 +142,7 @@ export default function Dashboard() {
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${sumByKey(dealList, "deal_amount")}</div>
<div className="text-2xl font-bold">${totalDealAmount}</div>
{/* <p className="text-xs text-muted-foreground">
+20.1% from last month
</p> */}
@ -79,9 +150,7 @@ export default function Dashboard() {
</Card>
<Card>
<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
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@ -105,9 +174,7 @@ export default function Dashboard() {
</Card>
<Card>
<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
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@ -162,23 +229,14 @@ export default function Dashboard() {
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview graphType={graphType} graphData={graphData} />
<Overview graphType={graphType} data={data} />
{/* tab to switch between line and bar graph */}
<Tabs
defaultValue="line"
className="space-y-4 ml-[50%] mt-2"
>
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
<TabsList>
<TabsTrigger
value="line"
onClick={() => setGraphType("line")}
>
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
Line
</TabsTrigger>
<TabsTrigger
value="bar"
onClick={() => setGraphType("bar")}
>
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
Bar
</TabsTrigger>
</TabsList>
@ -188,13 +246,10 @@ export default function Dashboard() {
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Funds</CardTitle>
<CardDescription>
You made {dealList?.length || 0} sales this month.
</CardDescription>
<CardDescription>You made {dealList?.length || 0} sales this month.</CardDescription>
</CardHeader>
<CardContent>
<RecentFunds recentDealData={recentDealData}>
</RecentFunds>
<RecentFunds></RecentFunds>
</CardContent>
</Card>
</div>

View File

@ -1,22 +1,4 @@
import { Loader } from "@/components/loading/loader";
export default function Loading() {
return (
<div className="container flex items-center justify-center h-screen">
<div className="text-center">
<svg
className="animate-spin h-12 w-12 text-gray-600 mx-auto mb-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291l-2.832 2.832A10.003 10.003 0 0112 22v-4a8.001 8.001 0 01-6-5.709z"
></path>
</svg>
<p className="text-lg font-semibold text-gray-600">Loading data...</p>
</div>
</div>
);
return <Loader />;
}

View File

@ -81,14 +81,14 @@ export default async function Home() {
<CardTitle className="text-lg md:text-2xl">Follow Us</CardTitle>
</CardHeader>
<CardContent className="flex gap-2">
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
Github
</Button>
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
Github
</Button>
<Link href="https://github.com/Sosokker/B2D-Ventures" passHref>
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<div className="dark:bg-white rounded-full">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
</div>
Github
</Button>
</Link>
</CardContent>
</Card>
</div>

View File

@ -0,0 +1,245 @@
import { Overview } from "@/components/ui/overview";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { getInvestorDeal } from "@/lib/data/investmentQuery";
import PieChart from "@/components/pieChart";
import {
overAllGraphData,
fourYearGraphData,
dayOftheWeekData,
getInvestorProjectTag,
countTags,
getBusinessTypeName,
countValues,
checkForInvest,
getLatestInvestment,
getTotalInvestment,
} from "./query";
import CountUpComponent from "@/components/countUp";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { RecentFunds } from "@/components/recent-funds";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import QuestionMarkIcon from "@/components/icon/questionMark";
import { NoDataAlert } from "@/components/alert/noData/alert";
import { error } from "console";
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
export default async function Portfolio({ params }: { params: { uid: string } }) {
const supabase = createSupabaseClient();
// if user hasn't invest in anything
const hasInvestments = await checkForInvest(supabase, params.uid);
if (!hasInvestments) {
return (
<div>
<NoDataAlert />
</div>
);
}
const { data: deals, error: investorDealError } = await getInvestorDeal(supabase, params.uid);
if (investorDealError) {
console.error(investorDealError);
}
const { data: localUser, error: localUserError } = await supabase.auth.getUser();
if (localUserError) {
console.error("Error while fetching user" + error);
}
// block user from try to see other user portfolio
if (params.uid != localUser.user?.id) {
return (
<>
<UnAuthorizedAlert />
</>
);
}
const username = localUser ? localUser.user.user_metadata.name : "Anonymous";
// console.log(username)
const overAllData = deals ? overAllGraphData(deals) : [];
const fourYearData = deals ? fourYearGraphData(deals) : [];
const dayOfWeekData = deals ? dayOftheWeekData(deals) : [];
const tags = deals ? await getInvestorProjectTag(supabase, deals) : [];
const latestDeals = deals
? await Promise.all(
(await getLatestInvestment(supabase, deals)).map(async (deal) => ({
...deal,
logo_url: await deal.logo_url,
}))
)
: [];
const totalInvestment = deals ? getTotalInvestment(deals) : 0;
// console.log(latestDeals);
const tagCount = countTags(tags);
// console.log(investedBusinessIds);
const businessType = deals
? await Promise.all(deals.map(async (item) => await getBusinessTypeName(supabase, item.project_id)))
: [];
const countedBusinessType = countValues(businessType.filter((item) => item !== null));
// console.log(countedBusinessType);
// console.log(tagCount);
return (
<div className="p-5">
{/* {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">
<h1 className="text-2xl font-semibold">Welcome to your Portfolio, {username}!</h1>
<p className="text-lg text-muted-foreground">
Here&lsquo;s an overview of your investment journey and progress.
</p>
<p className="text-xl font-medium text-green-400">
Total Investment: $
<CountUpComponent end={totalInvestment} duration={1} />
</p>
</div>
<div className="flex flew-rows-3 gap-10 mt-5 w-full">
<Tabs defaultValue="daily" className="space-y-4 w-full">
<TabsList className="grid w-96 grid-cols-3">
<TabsTrigger value="daily">Daily</TabsTrigger>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="yearly">Yearly</TabsTrigger>
</TabsList>
<TabsContent value="monthly">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-md font-bold">Monthly Investment Trend</CardTitle>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<QuestionMarkIcon />
</TooltipTrigger>
<TooltipContent>
<p>
Displays total investments each month over the past 12 <br />
months, up to today.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent className="mt-5">
<Overview graphType="line" data={overAllData} graphHeight={500}></Overview>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="yearly">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-md font-bold">Yearly Investment Summary</CardTitle>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<QuestionMarkIcon />
</TooltipTrigger>
<TooltipContent>
<p>
Shows total investments for each of the last four years, <br />
including the current year to date.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent className="mt-5">
<Overview graphType="bar" data={fourYearData} graphHeight={500}></Overview>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="daily">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-md font-bold">Daily Investment Breakdown</CardTitle>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<QuestionMarkIcon />
</TooltipTrigger>
<TooltipContent>
<p>
Illustrates total investments for each day over the past <br />
year, up to today.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent className="mt-5">
<Overview graphType="bar" data={dayOfWeekData} graphHeight={500}></Overview>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<div className="flex flex-cols-3 w-full gap-5 mt-5">
<Card className="w-1/3">
<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>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<QuestionMarkIcon />
</TooltipTrigger>
<TooltipContent>
<p>
Displays the distribution of project tags in your <br />
investments, highlighting areas of interest.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent className="mt-5">
<PieChart
data={tagCount.map((item: { name: string; count: number }) => item.count)}
labels={tagCount.map((item: { name: string; count: number }) => item.name)}
header="Total"
/>
</CardContent>
</Card>
<Card className="w-1/3">
<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>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<QuestionMarkIcon />
</TooltipTrigger>
<TooltipContent>
<p>
Shows the breakdown of business types in your portfolio, <br />
illustrating sector diversity.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent className="mt-5">
<PieChart
data={Object.values(countedBusinessType)}
labels={Object.keys(countedBusinessType)}
header="Total"
/>
</CardContent>
</Card>
<Card className="w-1/3">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-md font-bold">Recent investment</CardTitle>
</CardHeader>
<CardContent className="mt-5">
<RecentFunds data={latestDeals} />
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -0,0 +1,256 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { getProjectTag, getTagName } from "@/lib/data/tagQuery";
async function fetchLogoURL(supabase: SupabaseClient, projectId: number) {
const logoIndex = 1;
let { data: project_material, error } = await supabase
.from("project_material")
.select("material_url")
.eq("project_id", projectId)
.eq("material_type_id", logoIndex);
if (error) {
console.error("Error while fetching golo url" + error);
}
if (project_material && project_material.length > 0) {
return project_material[0].material_url;
}
return "";
}
function getTotalInvestment(deals: { deal_amount: number }[]) {
let total = 0;
for (let index = 0; index < deals.length; index++) {
total += deals[index].deal_amount;
}
return total;
}
async function getLatestInvestment(
supabase: SupabaseClient,
deals: { project_id: number; deal_amount: number; created_time: Date }[]
) {
const llist = [];
const count = 8;
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);
if (error) {
console.error(error);
}
let url = fetchLogoURL(supabase, deals[i].project_id);
llist.push({
name: project?.[0]?.project_name,
amount: deals[i].deal_amount,
date: new Date(deals[i].created_time),
logo_url: url,
});
}
return llist;
}
async function checkForInvest(supabase: SupabaseClient, userId: string) {
let { count, error } = await supabase
.from("investment_deal")
.select("*", { count: "exact" })
.eq("investor_id", userId);
if (error) {
console.error(error);
return false;
}
// if user already invest in something
if (count !== null && count > 0) {
return true;
}
return false;
}
function countValues(arr: { value: string }[][]): Record<string, number> {
const counts: Record<string, number> = {};
arr.forEach((subArray) => {
subArray.forEach((item) => {
const value = item.value;
counts[value] = (counts[value] || 0) + 1;
});
});
return counts;
}
async function getBusinessTypeName(supabase: SupabaseClient, projectId: number) {
// step 1: get business id from project id
let { data: project, error: projectError } = await supabase.from("project").select("business_id").eq("id", projectId);
if (projectError) {
console.error(projectError);
}
// step 2: get business type's id from business id
let { data: business, error: businessError } = await supabase
.from("business")
.select("business_type")
.eq("id", project?.[0]?.business_id);
if (businessError) {
console.error(businessError);
}
// step 3: get business type from its id
let { data: business_type, error: businessTypeError } = await supabase
.from("business_type")
.select("value")
.eq("id", business?.[0]?.business_type);
if (businessTypeError) {
console.error(businessError);
}
return business_type;
}
// only use deal that were made at most year ago
interface Deal {
created_time: string | number | Date;
deal_amount: any;
}
interface GraphData {
name: string;
value: number;
}
function overAllGraphData(deals: Deal[]): GraphData[] {
// Initialize all months with value 0
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const acc: GraphData[] = months.map((month) => ({ name: month, value: 0 }));
deals
.filter((item: Deal) => new Date(item.created_time) >= yearAgo(1))
.forEach((item: Deal) => {
const monthName = getMonthName(item.created_time.toString()).slice(0, 3);
const monthEntry = acc.find((entry) => entry.name === monthName);
if (monthEntry) {
monthEntry.value += item.deal_amount;
}
});
return acc;
}
function fourYearGraphData(deals: Deal[]): GraphData[] {
const currentYear = new Date().getFullYear();
const acc: GraphData[] = Array.from({ length: 4 }, (_, i) => ({
name: (currentYear - i).toString(),
value: 0,
})).reverse();
deals
.filter((item: Deal) => new Date(item.created_time) >= yearAgo(3))
.forEach((item: Deal) => {
const year = new Date(item.created_time).getFullYear().toString();
const yearEntry = acc.find((entry) => entry.name === year);
if (yearEntry) {
yearEntry.value += item.deal_amount;
}
});
return acc;
}
interface DayOfWeekData {
name: string;
value: number;
}
function dayOftheWeekData(deals: Deal[]): DayOfWeekData[] {
const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const dayOfWeekData: DayOfWeekData[] = daysOfWeek.map((day) => ({
name: day,
value: 0,
}));
deals
.filter((item: Deal) => new Date(item.created_time) >= yearAgo(1))
.forEach((item: Deal) => {
const day = getDayAbbreviation(item.created_time);
const dayEntry = dayOfWeekData.find((entry) => entry.name === day);
if (dayEntry) {
dayEntry.value += item.deal_amount;
}
});
return dayOfWeekData;
}
async function getInvestorProjectTag(supabase: SupabaseClient, deals: number | { project_id: number }[]) {
// get unique project id from deals
const uniqueProjectIds: number[] = Array.isArray(deals)
? Array.from(new Set(deals.map((deal: { project_id: number }) => deal.project_id)))
: [];
const tagIds = (
await Promise.all(
uniqueProjectIds.map(async (projectId: number) => {
const { data: tagIdsArray, error: tagError } = await getProjectTag(supabase, projectId);
if (tagError) {
console.error(tagError);
return [];
}
return tagIdsArray?.map((tag: { tag_id: any }) => tag.tag_id) || [];
})
)
).flat();
// console.log(tagIds, uniqueProjectIds);
const tagNames = await Promise.all(
tagIds
.filter((tagId) => tagId !== null)
.map(async (id: number) => {
const { data: tagName, error: nameError } = await getTagName(supabase, id);
if (nameError) {
console.error(nameError);
return null;
}
return tagName;
})
);
// console.log(tagNames);
return tagNames.filter((tagName) => tagName !== null);
}
const countTags = (tags: any[]) => {
const tagCounts = tags.flat().reduce(
(acc, tag) => {
const tagName = tag.value;
acc[tagName] = (acc[tagName] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return Object.entries(tagCounts).map(([name, count]) => ({
name,
count: count as number,
}));
};
const getDayAbbreviation = (dateString: string | number | Date) => {
const date = new Date(dateString);
return date.toLocaleString("default", { weekday: "short" });
};
const yearAgo = (num: number) => {
const newDate = new Date();
newDate.setFullYear(newDate.getFullYear() - num);
return newDate;
};
const getMonthName = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString("default", { month: "long", year: "numeric" });
};
export {
overAllGraphData,
fourYearGraphData,
dayOftheWeekData,
getInvestorProjectTag,
countTags,
getBusinessTypeName,
countValues,
checkForInvest,
getLatestInvestment,
getTotalInvestment,
fetchLogoURL,
};

View File

@ -9,22 +9,14 @@ import { businessFormSchema } from "@/types/schemas/application.schema";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@radix-ui/react-tooltip";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
type businessSchema = z.infer<typeof businessFormSchema>;
interface BusinessFormProps {
applyProject: boolean;
setApplyProject: Function;
onSubmit: SubmitHandler<businessSchema>;
}
const BusinessForm = ({
applyProject,
setApplyProject,
onSubmit,
}: BusinessFormProps & { onSubmit: SubmitHandler<businessSchema> }) => {
const BusinessForm = ({ onSubmit }: BusinessFormProps & { onSubmit: SubmitHandler<businessSchema> }) => {
const communitySize = [
{ id: 1, name: "N/A" },
{ id: 2, name: "0-5K" },
@ -385,24 +377,6 @@ const BusinessForm = ({
</FormItem>
)}
/>
<div className="flex space-x-5">
<Switch onCheckedChange={() => setApplyProject(!applyProject)}></Switch>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-[12px] text-neutral-500 self-center cursor-pointer">
Would you like to apply for your first fundraising project as well?
</span>
</TooltipTrigger>
<TooltipContent>
<p className="text-[11px]">
Toggling this option allows you to begin your first project, <br /> which is crucial for unlocking
the tools necessary to raise funds.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<center>
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5" type="submit">
Submit application

View File

@ -2,14 +2,7 @@ import { useEffect, useState } from "react";
import { SubmitHandler, useForm, ControllerRenderProps } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { MultipleOptionSelector } from "@/components/multipleSelector";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { projectFormSchema } from "@/types/schemas/application.schema";
import { z } from "zod";
@ -17,19 +10,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Label } from "@/components/ui/label";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { Textarea } from "./ui/textarea";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { ChevronsUpDown, Check, X } from "lucide-react";
@ -39,30 +21,21 @@ type FieldType = ControllerRenderProps<any, "projectPhotos">;
interface ProjectFormProps {
onSubmit: SubmitHandler<projectSchema>;
}
const ProjectForm = ({
onSubmit,
}: ProjectFormProps & { onSubmit: SubmitHandler<projectSchema> }) => {
const ProjectForm = ({ onSubmit }: ProjectFormProps & { onSubmit: SubmitHandler<projectSchema> }) => {
const form = useForm<projectSchema>({
resolver: zodResolver(projectFormSchema),
defaultValues: {},
});
let supabase = createSupabaseClient();
const [projectType, setProjectType] = useState<
{ id: number; name: string }[]
>([]);
const [projectType, setProjectType] = useState<{ id: number; name: string }[]>([]);
const [projectPitch, setProjectPitch] = useState("text");
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [projectPitchFile, setProjectPitchFile] = useState("");
const [tag, setTag] = useState<{ id: number; value: string }[]>([]);
const [open, setOpen] = useState(false);
const [selectedTag, setSelectedTag] = useState<
{ id: number; value: string }[]
>([]);
const [selectedTag, setSelectedTag] = useState<{ id: number; value: string }[]>([]);
const handleFileChange = (
event: React.ChangeEvent<HTMLInputElement>,
field: FieldType
) => {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, field: FieldType) => {
if (event.target.files) {
const filesArray = Array.from(event.target.files);
console.log("first file", filesArray);
@ -86,9 +59,7 @@ const ProjectForm = ({
};
const fetchProjectType = async () => {
let { data: ProjectType, error } = await supabase
.from("project_type")
.select("id, value");
let { data: ProjectType, error } = await supabase.from("project_type").select("id, value");
if (error) {
console.error(error);
@ -125,10 +96,7 @@ const ProjectForm = ({
}, []);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)}
className="space-y-8"
>
<form onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)} className="space-y-8">
<div className="ml-96 space-y-10">
{/* project name */}
<FormField
@ -137,17 +105,10 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="space-y-5">
<FormLabel className="font-bold text-lg">
Project name
</FormLabel>
<FormLabel className="font-bold text-lg">Project name</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="text"
id="projectName"
className="w-96"
{...field}
/>
<Input type="text" id="projectName" className="w-96" {...field} />
</div>
</FormControl>
</div>
@ -169,9 +130,7 @@ const ProjectForm = ({
handleFunction={(selectedValues: any) => {
field.onChange(selectedValues.id);
}}
description={
<>Please specify the primary purpose of the funds</>
}
description={<>Please specify the primary purpose of the funds</>}
placeholder="Select a Project type"
selectLabel="Project type"
/>
@ -189,18 +148,11 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Short description
</FormLabel>
<FormLabel className="font-bold text-lg">Short description</FormLabel>
<div className="flex space-x-5">
<Textarea
id="shortDescription"
className="w-96"
{...field}
/>
<Textarea id="shortDescription" className="w-96" {...field} />
<span className="text-[12px] text-neutral-500 self-center">
Could you provide a brief description of your project{" "}
<br /> in one or two sentences?
Could you provide a brief description of your project <br /> in one or two sentences?
</span>
</div>
</div>
@ -225,9 +177,7 @@ const ProjectForm = ({
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={
projectPitch === "text" ? "default" : "outline"
}
variant={projectPitch === "text" ? "default" : "outline"}
onClick={() => setProjectPitch("text")}
className="w-32 h-12 text-base"
>
@ -235,9 +185,7 @@ const ProjectForm = ({
</Button>
<Button
type="button"
variant={
projectPitch === "file" ? "default" : "outline"
}
variant={projectPitch === "file" ? "default" : "outline"}
onClick={() => setProjectPitch("file")}
className="w-32 h-12 text-base"
>
@ -247,11 +195,7 @@ const ProjectForm = ({
<div className="flex space-x-5">
<Input
type={projectPitch === "file" ? "file" : "text"}
placeholder={
projectPitch === "file"
? "Upload your Markdown file"
: "https:// "
}
placeholder={projectPitch === "file" ? "Upload your Markdown file" : "https:// "}
accept={projectPitch === "file" ? ".md" : undefined}
onChange={(e) => {
const value = e.target;
@ -266,11 +210,9 @@ const ProjectForm = ({
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload a file or paste a link to your pitch,
which should <br />
cover key aspects of your project: what it will do,
what investors <br /> can expect to gain, and any
highlights that make it stand out.
Please upload a file or paste a link to your pitch, which should <br />
cover key aspects of your project: what it will do, what investors <br /> can expect to gain,
and any highlights that make it stand out.
</span>
</div>
{projectPitchFile && (
@ -302,23 +244,22 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project logo
</FormLabel>
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || "");
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
</span>
<FormLabel className="font-bold text-lg mt-10">Project logo</FormLabel>
<div className="flex space-x-5">
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || "");
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your project.
</span>
</div>
</div>
</FormControl>
<FormMessage />
@ -334,9 +275,7 @@ const ProjectForm = ({
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project photos
</FormLabel>
<FormLabel className="font-bold text-lg mt-10">Project photos</FormLabel>
<div className="flex space-x-5">
<Input
type="file"
@ -349,16 +288,15 @@ const ProjectForm = ({
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
Please upload the photo that best represents your project.
<p className="text-red-500">
*** It is recommended that the photo be horizontal for better presentation.
</p>
</span>
</div>
<div className="mt-5 space-y-2 w-96">
{selectedImages.map((image, index) => (
<div
key={index}
className="flex justify-between items-center border p-2 rounded"
>
<div key={index} className="flex justify-between items-center border p-2 rounded">
<span>{image.name}</span>
<Button
variant="outline"
@ -385,9 +323,7 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Minimum investment
</FormLabel>
<FormLabel className="font-bold text-lg">Minimum investment</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
@ -419,9 +355,7 @@ const ProjectForm = ({
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Target investment
</FormLabel>
<FormLabel className="font-bold text-lg">Target investment</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
@ -437,8 +371,8 @@ const ProjectForm = ({
value={field.value}
/>
<span className="text-[12px] text-neutral-500 self-center">
We encourage you to set a specific target investment{" "}
<br /> amount that reflects your funding goals.
We encourage you to set a specific target investment <br /> amount that reflects your funding
goals.
</span>
</div>
</FormControl>
@ -457,16 +391,10 @@ const ProjectForm = ({
<FormLabel className="font-bold text-lg">Deadline</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="datetime-local"
id="deadline"
className="w-96"
{...field}
/>
<Input type="datetime-local" id="deadline" className="w-96" {...field} />
<span className="text-[12px] text-neutral-500 self-center">
What is the deadline for your fundraising project?
Setting <br /> a clear timeline can help motivate
potential investors.
What is the deadline for your fundraising project? Setting <br /> a clear timeline can help
motivate potential investors.
</span>
</div>
</FormControl>
@ -493,9 +421,7 @@ const ProjectForm = ({
aria-expanded={open}
className="w-96 justify-between overflow-hidden text-ellipsis whitespace-nowrap"
>
{selectedTag.length > 0
? selectedTag.map((t) => t.value).join(", ")
: "Select tags..."}
{selectedTag.length > 0 ? selectedTag.map((t) => t.value).join(", ") : "Select tags..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@ -511,15 +437,11 @@ const ProjectForm = ({
value={tag.value}
onSelect={() => {
setSelectedTag((prev) => {
const exists = prev.find(
(t) => t.id === tag.id
);
const exists = prev.find((t) => t.id === tag.id);
const updatedTags = exists
? prev.filter((t) => t.id !== tag.id)
: [...prev, tag];
field.onChange(
updatedTags.map((t) => t.id)
);
field.onChange(updatedTags.map((t) => t.id));
return updatedTags;
});
setOpen(false);
@ -528,9 +450,7 @@ const ProjectForm = ({
<Check
className={cn(
"h-4",
selectedTag.some((t) => t.id === tag.id)
? "opacity-100"
: "opacity-0"
selectedTag.some((t) => t.id === tag.id) ? "opacity-100" : "opacity-0"
)}
/>
{tag.value}
@ -542,8 +462,7 @@ const ProjectForm = ({
</PopoverContent>
</Popover>
<span className="text-[12px] text-neutral-500 self-center">
Add 1 to 5 tags that describe your project. Tags help{" "}
<br />
Add 1 to 5 tags that describe your project. Tags help <br />
investors understand your focus.
</span>
</div>
@ -561,9 +480,7 @@ const ProjectForm = ({
<button
onClick={() => {
setSelectedTag((prev) => {
const updatedTags = prev.filter(
(t) => t.id !== tag.id
);
const updatedTags = prev.filter((t) => t.id !== tag.id);
field.onChange(updatedTags.map((t) => t.id));
return updatedTags;
});
@ -579,10 +496,7 @@ const ProjectForm = ({
/>
</div>
<center>
<Button
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5 "
type="submit"
>
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5 " type="submit">
Submit application
</Button>
</center>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
"use client";
import Lottie from "react-lottie";
import * as alertData from "./alert.json";
const alertOption = {
loop: true,
autoplay: true,
animationData: alertData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
export function NoDataAlert() {
return (
<div className="fixed inset-0 flex items-center justify-centerbg-black mt-10">
<Lottie options={alertOption} height={"80%"} width={"50%"} />
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
"use client";
import Lottie from "react-lottie";
import * as alertData from "./alert.json";
const alertOption = {
loop: true,
autoplay: true,
animationData: alertData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
export function UnAuthorizedAlert() {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black mt-24 z-50">
<Lottie options={alertOption} style={{ width: "50%", height: "auto" }} />
</div>
);
}

View File

@ -0,0 +1,19 @@
"use client";
import CountUp from "react-countup";
interface CountUpComponentProps {
end: number;
duration: number;
}
export default function CountUpComponent(props: CountUpComponentProps) {
return (
<>
<CountUp
end={props.end}
duration={props.duration}
start={props.end / 2}
/>
</>
);
}

View File

@ -0,0 +1,17 @@
export default function QuestionMarkIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
version="1.1"
id="_x32_"
viewBox="0 0 512 512"
fill="currentColor"
>
<g>
<path d="M256,0C114.616,0,0,114.612,0,256s114.616,256,256,256s256-114.612,256-256S397.385,0,256,0z M207.678,378.794 c0-17.612,14.281-31.893,31.893-31.893c17.599,0,31.88,14.281,31.88,31.893c0,17.595-14.281,31.884-31.88,31.884 C221.959,410.678,207.678,396.389,207.678,378.794z M343.625,218.852c-3.596,9.793-8.802,18.289-14.695,25.356 c-11.847,14.148-25.888,22.718-37.442,29.041c-7.719,4.174-14.533,7.389-18.769,9.769c-2.905,1.604-4.479,2.95-5.256,3.826 c-0.768,0.926-1.029,1.306-1.496,2.826c-0.273,1.009-0.558,2.612-0.558,5.091c0,6.868,0,12.512,0,12.512 c0,6.472-5.248,11.728-11.723,11.728h-28.252c-6.475,0-11.732-5.256-11.732-11.728c0,0,0-5.645,0-12.512 c0-6.438,0.752-12.744,2.405-18.777c1.636-6.008,4.215-11.718,7.508-16.694c6.599-10.083,15.542-16.802,23.984-21.48 c7.401-4.074,14.723-7.455,21.516-11.281c6.789-3.793,12.843-7.91,17.302-12.372c2.988-2.975,5.31-6.05,7.087-9.52 c2.335-4.628,3.955-10.067,3.992-18.389c0.012-2.463-0.698-5.702-2.632-9.405c-1.926-3.686-5.066-7.694-9.264-11.29 c-8.45-7.248-20.843-12.545-35.054-12.521c-16.285,0.058-27.186,3.876-35.587,8.62c-8.36,4.776-11.029,9.595-11.029,9.595 c-4.268,3.718-10.603,3.85-15.025,0.314l-21.71-17.397c-2.719-2.173-4.322-5.438-4.396-8.926c-0.063-3.479,1.425-6.81,4.061-9.099 c0,0,6.765-10.43,22.451-19.38c15.62-8.992,36.322-15.488,61.236-15.429c20.215,0,38.839,5.562,54.268,14.661 c15.434,9.148,27.897,21.744,35.851,36.876c5.281,10.074,8.525,21.43,8.533,33.38C349.211,198.042,347.248,209.058,343.625,218.852 z" />
</g>
</svg>
);
}

View File

@ -1,3 +1,5 @@
"use client";
import Lottie from "react-lottie";
import * as loadingData from "./loading.json";
@ -11,17 +13,17 @@ const loadingOption = {
};
interface LoaderProps {
isSuccess: boolean;
isSuccess?: boolean;
}
export function Loader(props: LoaderProps) {
return (
<>
<div>
{!props.isSuccess && (
<div className="fixed inset-0 flex items-center justify-center bg-white bg-opacity-10 backdrop-blur-sm z-50">
<Lottie options={loadingOption} height={200} width={200} />
</div>
)}
</>
</div>
);
}

View File

@ -30,14 +30,16 @@ export const AuthenticatedComponents = ({ uid }: AuthenticatedComponentsProps) =
<div className={`flex gap-3 pl-2 items-center ${businessClass}`}>
<Link href={"/notification"}>
<div className="relative inline-block">
<Bell className="h-6 w-6" />
<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">
<Bell className="h-6 w-6 " />
<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">
{displayValue}
</span>
</div>
</Link>
<Heart />
<Wallet />
<Link href={"/portfolio/" + uid}>
<Wallet className="cursor-pointer" />
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="overflow-hidden rounded-full">
@ -59,6 +61,11 @@ export const AuthenticatedComponents = ({ uid }: AuthenticatedComponentsProps) =
<Link href="/admin">Admin</Link>
</DropdownMenuItem>
)}
{data != null && data != undefined && data.role === "business" && (
<DropdownMenuItem>
<Link href="/dataroom/manage">Dataroom</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogoutButton />

View File

@ -115,7 +115,9 @@ export async function NavigationBar() {
</NavigationMenu>
<div className="flex gap-2 pl-2">
<ThemeToggle />
<div className="mt-1">
<ThemeToggle />
</div>
<Separator orientation="vertical" className="mx-3" />
{userId ? <AuthenticatedComponents uid={userId} /> : <UnAuthenticatedComponents />}
</div>

View File

@ -0,0 +1,42 @@
"use client";
import { Pie } from "react-chartjs-2";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
ChartJS.register(ArcElement, Tooltip, Legend);
interface PieChartProps {
labels: string[];
data: number[];
header: string;
}
const PieChart = (props: PieChartProps) => {
const chartData = {
labels: props.labels,
datasets: [
{
label: props.header,
data: props.data,
backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56"],
hoverBackgroundColor: ["#FF6384", "#36A2EB", "#FFCE56"],
borderWidth: 1,
},
],
};
const options = {
plugins: {
legend: {
position: "bottom" as const,
},
},
};
return (
<>
<Pie data={chartData} options={options} />
</>
);
};
export default PieChart;

View File

@ -5,35 +5,30 @@ export type RecentDealData = {
deal_amount: number;
investor_id: string;
username: string;
avatar_url?: string;
logo_url?: string;
// email: string;
};
interface RecentFundsProps {
recentDealData: RecentDealData[];
data?: { name?: string; amount?: number; avatar?: string; date?: Date; logo_url?: string }[];
}
export function RecentFunds({ recentDealData }: RecentFundsProps) {
export function RecentFunds(props: RecentFundsProps) {
return (
<div className="space-y-8">
{recentDealData?.length > 0 ? (
recentDealData.map((data) => (
<div className="flex items-center" key={data.investor_id}>
<Avatar className="h-9 w-9">
<AvatarImage src={data.avatar_url} alt={data.username} />
{/* #TODO make this not quick fix */}
<AvatarFallback>{data.username ? data.username[0]: ""}</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{data.username}</p>
{/* <p className="text-sm text-muted-foreground">{data.email}</p> */}
</div>
<div className="ml-auto font-medium">+${data.deal_amount}</div>
{(props?.data || []).map((deal, index) => (
<div className="flex items-center" key={index}>
<Avatar className="h-9 w-9">
<AvatarImage src={deal.logo_url} 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>
</div>
))
) : (
<p>No recent deals available.</p>
)}
<div className="ml-auto font-medium">+${deal.amount}</div>
</div>
))}
</div>
);
}

View File

@ -21,7 +21,10 @@ export function SiteFooter() {
<Link href="/services" className="hover:underline">
Services
</Link>
<Link href="/contact" className="hover:underline">
<Link
href="mailto:b2d.ventures.contact@gmail.com"
className="hover:underline"
>
Contact
</Link>
</div>

View File

@ -1,66 +1,108 @@
"use client";
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, LineChart, Line } from "recharts";
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, LineChart, Line, Tooltip } from "recharts";
// const data = [
// {
// name: "Jan",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Feb",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Mar",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Apr",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "May",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Jun",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Jul",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Aug",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Sep",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Oct",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Nov",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// {
// name: "Dec",
// total: Math.floor(Math.random() * 5000) + 1000,
// },
// ];
interface OverViewProps {
graphType: string;
graphData: Record<string, number>; // Object with month-year as keys and sum as value
data: { name: string; value: number }[];
graphHeight?: number | string;
}
export function Overview(props: OverViewProps) {
// Transform the grouped data into the format for the chart
const chartData = Object.entries(props.graphData).map(([monthYear, totalArray]) => ({
name: monthYear,
total: totalArray, // Get the total amount for the month
}));
return (
<ResponsiveContainer width="100%" height={350}>
{props.graphType === 'line' ? (
<LineChart data={chartData}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Line
dataKey="total"
fill="currentColor"
className="fill-primary"
/>
</LineChart>
) : (
<BarChart data={chartData}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Bar
dataKey="total"
fill="currentColor"
className="fill-primary"
/>
</BarChart>
)}
</ResponsiveContainer>
);
return (
<ResponsiveContainer width="100%" height={props.graphHeight || 350}>
{props.graphType === "line" ? (
<LineChart data={props.data}>
<XAxis dataKey="name" stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip
formatter={(value) => `$${value}`}
contentStyle={{
backgroundColor: "#f5f5f5",
borderRadius: "5px",
color: "#000",
}}
/>
<Line dataKey="value" fill="currentColor" className="fill-primary" />
</LineChart>
) : (
<BarChart data={props.data}>
<XAxis dataKey="name" stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip
formatter={(value) => `$${value}`}
contentStyle={{
backgroundColor: "#f5f5f5",
borderRadius: "5px",
color: "#000",
}}
/>
<Bar dataKey="value" fill="currentColor" className="fill-primary" radius={[15, 15, 0, 0]} />
</BarChart>
)}
</ResponsiveContainer>
);
}

View File

@ -18,6 +18,17 @@ export const getAllBusinesses = (client: SupabaseClient) => {
`);
};
export async function getBusinessByUserId(client: SupabaseClient, userId: string) {
const { data, error } = await client.from("business").select("*").eq("user_id", userId);
if (error) {
console.error("Error fetching business ID:", error);
return null;
}
return data;
}
export const getBusinessByName = (
client: SupabaseClient,
params: { businessName?: string | null; single?: boolean } = { single: false }

View File

@ -29,3 +29,11 @@ export const getInvestmentByUserId = (client: SupabaseClient, userId: string) =>
)
.eq("investor_id", userId);
};
export function getInvestorDeal(client: SupabaseClient, userId: string) {
return client
.from("investment_deal")
.select("*")
.in("investor_id", [userId])
.order("created_time", { ascending: true });
}

View File

@ -25,7 +25,7 @@ async function getTopProjects(client: SupabaseClient, numberOfRecords: number =
if (error) {
return { data: null, error: error.message };
}
// console.log(data);
return { data, error: null };
} catch (err) {
return { data: null, error: "An unexpected error occurred." };

0
src/lib/data/query.ts Normal file
View File

View File

@ -3,3 +3,11 @@ import { SupabaseClient } from "@supabase/supabase-js";
export const getTagsByProjectIds = (client: SupabaseClient, projectIds: string[]) => {
return client.from("project_tag").select(`item_id, ...tag (tag_value:value)`).in("item_id", projectIds);
};
export function getProjectTag(client: SupabaseClient, projectId: number) {
return client.from("project_tag").select("tag_id").in("item_id", [projectId]);
}
export function getTagName(client: SupabaseClient, tagId: number) {
return client.from("tag").select("value").in("id", [tagId]);
}