Merge remote-tracking branch 'origin/back-end' into back-end

This commit is contained in:
Nantawat Sukrisunt 2024-11-17 13:56:04 +07:00
commit 6408d071ca
31 changed files with 2985 additions and 746 deletions

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
/tailwind.config.ts

1914
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,10 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@mdxeditor/editor": "^3.15.0",
"@nextui-org/calendar": "^2.0.12",
"@nextui-org/date-input": "^2.1.4",
"@nextui-org/system": "^2.2.6",
"@nextui-org/theme": "^2.2.11",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
@ -32,25 +36,28 @@
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^4.7.0",
"@supabase-cache-helpers/postgrest-react-query": "^1.10.1",
"@supabase/ssr": "^0.4.1",
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.46.1",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/react-query": "^5.59.0",
"@tanstack/react-query-devtools": "^5.59.0",
"@tanstack/react-table": "^8.20.5",
"b2d-ventures": "file:",
"chart.js": "^4.4.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.0.0",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.2.0",
"framer-motion": "^11.11.17",
"lucide-react": "^0.428.0",
"next": "^14.2.15",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-countup": "^6.5.3",
"react-day-picker": "^9",
"react-day-picker": "^9.*",
"react-dom": "^18",
"react-file-icon": "^1.5.0",
"react-hook-form": "^7.53.0",

View File

@ -1,42 +0,0 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import Image from "next/image";
import { StaticImport } from "next/dist/shared/lib/get-img-props";
interface ItemProps {
src: string | StaticImport;
alt: string;
width: number;
height: number;
className?: string;
}
const ImageModal = ({ src, alt, width, height, className }: ItemProps) => {
return (
<Dialog>
<DialogTrigger asChild>
<Image src={src} alt={alt} width={width} height={height} className={className} />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Image Preview</DialogTitle>
<DialogDescription>Click outside to close the image preview.</DialogDescription>
</DialogHeader>
<Image src={src} alt={alt} width={700} height={400} />
<DialogFooter />
</DialogContent>
</Dialog>
);
};
export function DisplayFullImage({ src, alt, width, height, className }: ItemProps) {
return <ImageModal src={src} alt={alt} width={width} height={height} className={className} />;
}

View File

@ -3,7 +3,7 @@
/* eslint-disable */
import { useState, useEffect } from "react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import CustomTooltip from "@/components/customToolTip";
import { ShareIcon, StarIcon } from "lucide-react";
import { deleteFollow, getFollow, insertFollow } from "@/lib/data/followQuery";
import toast from "react-hot-toast";
@ -13,9 +13,10 @@ import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
interface FollowShareButtons {
userId: string;
projectId: number;
projectName: string;
}
const FollowShareButtons = ({ userId, projectId }: FollowShareButtons) => {
const FollowShareButtons = ({ userId, projectId, projectName }: FollowShareButtons) => {
const supabase = createSupabaseClient();
const { data: follow, isLoading: followIsLoading } = useQuery(getFollow(supabase, userId, projectId), {
staleTime: 0,
@ -71,16 +72,9 @@ const FollowShareButtons = ({ userId, projectId }: FollowShareButtons) => {
return (
<div className="grid grid-cols-2 gap-5 justify-self-end ">
<div className="mt-2 cursor-pointer" onClick={handleFollow}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<StarIcon id="follow" fill={isFollowState ? "#fcb30e" : "#fff"} strokeWidth={2} />
</TooltipTrigger>
<TooltipContent>
<p>Follow NVIDIA</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<CustomTooltip message={`Follow ${projectName}`}>
<StarIcon id="follow" fill={isFollowState ? "#fcb30e" : "#fff"} strokeWidth={2} />
</CustomTooltip>
</div>
<div onClick={handleShare} className="cursor-pointer mt-2">
<ShareIcon />

View File

@ -1,8 +1,6 @@
import Image from "next/image";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import * as Tabs from "@radix-ui/react-tabs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
@ -10,7 +8,6 @@ import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import FollowShareButtons from "./followShareButton";
import { getProjectData } from "@/lib/data/projectQuery";
import { getDealList } from "@/app/api/dealApi";
import { sumByKey, toPercentage } from "@/lib/utils";
@ -89,7 +86,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
</span>
<FollowShareButtons userId={user!.user.id} projectId={params.id} />
<FollowShareButtons userId={user!.user.id} projectId={params.id} projectName={projectData?.project_name} />
</div>
{/* end of pack */}
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>

View File

@ -2,7 +2,7 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useState } from "react";
import { UserIcon, UsersIcon } from "lucide-react";
import { Building2, ClipboardPen, Tag } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { getAllBusinessTypeQuery, getAllTagsQuery, getAllProjectStatusQuery } from "@/lib/data/dropdownQuery";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
@ -140,7 +140,7 @@ export default function Deals() {
{/* Business Type Filter */}
<Select value={businessTypeFilter} onValueChange={(value) => setBusinessTypeFilter(value)}>
<SelectTrigger className="w-full sm:w-[180px]">
<UsersIcon className="ml-2" />
<Building2 className="ml-2" />
<SelectValue placeholder="Business Type" />
</SelectTrigger>
<SelectContent>
@ -169,7 +169,7 @@ export default function Deals() {
{/* Project Status Filter */}
<Select value={projectStatusFilter} onValueChange={(value) => setProjectStatusFilter(value)}>
<SelectTrigger className="w-full sm:w-[180px]">
<UserIcon className="ml-2" />
<ClipboardPen className="ml-2" />
<SelectValue placeholder="Project Status" />
</SelectTrigger>
<SelectContent>
@ -198,7 +198,7 @@ export default function Deals() {
{/* Tags Filter */}
<Select value={tagFilter} onValueChange={(value) => setTagFilter(value)}>
<SelectTrigger className="w-full sm:w-[180px]">
<UserIcon className="ml-2" />
<Tag className="ml-2" />
<SelectValue placeholder="Tags" />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -13,10 +13,20 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Clock } from "lucide-react";
import { DateTimePicker, TimePicker } from "@/components/ui/datetime-picker";
import { Label } from "@/components/ui/label";
import { createCalendarEvent } from "./actions";
import { createCalendarEvent, createMeetingLog, getFreeDate } from "./actions";
import { Session } from "@supabase/supabase-js";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { TimeInput } from "@nextui-org/date-input";
import { Calendar, DateValue } from "@nextui-org/calendar";
import { TimeValue } from "@react-types/datepicker";
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
// import { useLocale } from "@react-aria/i18n";
import { getMeetingLog } from "./actions";
import toast from "react-hot-toast";
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
import { LegacyLoader } from "@/components/loading/LegacyLoader";
import { isEventOverlapping } from "./overlapEvent";
interface DialogProps {
children?: React.ReactNode;
@ -27,37 +37,126 @@ interface DialogProps {
modal?: boolean;
session: Session;
projectName: string;
projectId: number;
}
export function MeetEventDialog(props: DialogProps) {
const [eventDate, setEventDate] = useState<Date | undefined>(undefined);
const [startTime, setStartTime] = useState<Date | undefined>(undefined);
const [endTime, setEndTime] = useState<Date | undefined>(undefined);
const supabase = createSupabaseClient();
const timezone = getLocalTimeZone();
const [eventDate, setEventDate] = useState<CalendarDate | undefined>(undefined);
const [startTime, setStartTime] = useState<TimeValue | undefined>(undefined);
const [endTime, setEndTime] = useState<TimeValue | undefined>(undefined);
const [eventName, setEventName] = useState(`Meet with ${props.projectName}`);
const [eventDescription, setEventDescription] = useState(
"Meet and gather more information on business in B2DVentures"
);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [noteToBusiness, setNoteToBusiness] = useState<string>("");
const session = props.session;
const {
data: freeDate,
error: freeDateError,
isLoading: isLoadingFreeDate,
} = useQuery(getFreeDate(supabase, props.projectId), { enabled: !!props.projectId });
useEffect(() => {
if (props.projectName) {
setEventName(`Meet with ${props.projectName}`);
}
}, [props.projectName]);
const {
data: meetingLog,
error: meetingLogError,
isLoading: isLoadingMeetingLog,
} = useQuery(getMeetingLog(supabase, props.projectId), { enabled: !!props.projectId });
const handleCreateEvent = async () => {
if (!session || !eventDate || !startTime || !endTime || !eventName) {
alert("Please fill in all event details.");
toast.error("Please fill in all event details.");
return;
}
const startDate = new Date(eventDate);
startDate.setHours(startTime.getHours(), startTime.getMinutes());
setIsSubmitting(true);
const endDate = new Date(eventDate);
endDate.setHours(endTime.getHours(), endTime.getMinutes());
await createCalendarEvent(session, startDate, endDate, eventName, eventDescription);
props.onOpenChange?.(false);
try {
const startDate = eventDate.toDate(timezone);
startDate.setHours(startTime.hour, startTime.minute);
const endDate = eventDate.toDate(timezone);
endDate.setHours(endTime.hour, startTime.minute);
const existingEvents = (meetingLog || []).map((log) => ({
meet_date: log.meet_date,
start_time: log.start_time,
end_time: log.end_time,
}));
const hasOverlap = isEventOverlapping(eventDate, startTime, endTime, existingEvents);
if (hasOverlap) {
toast.error("This current selected date and time is overlaped with any existing events.");
return;
}
await createCalendarEvent(session, startDate, endDate, eventName, eventDescription);
const { status, error } = await createMeetingLog({
client: supabase,
meet_date: eventDate.toString().split("T")[0],
start_time: startTime.toString(),
end_time: endTime.toString(),
note: noteToBusiness,
userId: session.user.id,
projectId: props.projectId!,
});
if (!status) {
console.error("Meeting log error:", error);
toast.error("Failed to log the meeting. Please try again.");
return;
}
toast.success("Meeting event created successfully!");
props.onOpenChange?.(false);
} catch (error) {
console.error("Error creating event:", error);
toast.error("There was an error creating the event. Please try again.");
} finally {
setIsSubmitting(false);
}
};
const meetingLogRanges = (meetingLog || []).map((log) => {
const [year, month, day] = log.meet_date.split("-").map(Number);
const startDate = new CalendarDate(year, month, day);
const endDate = new CalendarDate(year, month, day);
return [startDate, endDate];
});
const freetimeLogRanges = (freeDate || []).map((log) => {
const [year, month, day] = log.meet_date.split("-").map(Number);
const startDate = new CalendarDate(year, month, day);
const endDate = new CalendarDate(year, month, day);
return [startDate, endDate];
});
// const disabledRanges = [...meetingLogRanges];
// const { locale } = useLocale();
const isDateUnavailable = (date: DateValue) => {
const isFreeDate = freetimeLogRanges.some(
(interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
);
const isMeetingDate = meetingLogRanges.some(
(interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
);
return !isFreeDate || isMeetingDate;
};
return (
<Dialog {...props}>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-md overflow-y-auto h-[80%]">
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
<Clock />
@ -68,53 +167,66 @@ export function MeetEventDialog(props: DialogProps) {
</DialogDescription>
</DialogHeader>
<div className="space-y-4 w-[90%]">
<div>
<Label htmlFor="eventName">Event Name</Label>
<Input
id="eventName"
placeholder="Enter event name"
value={eventName}
onChange={(e) => setEventName(e.target.value)}
/>
{meetingLogError || freeDateError ? (
<div className="error-message">
<p>Error loading meeting logs</p>
</div>
<div>
<Label htmlFor="eventDescription">Event Description</Label>
<Input
id="eventDescription"
placeholder="Enter event description"
value={eventDescription}
onChange={(e) => setEventDescription(e.target.value)}
/>
</div>
<div>
<Label htmlFor="eventDescription">Event Description</Label>
<Input
id="note"
placeholder="Your note to business"
value={noteToBusiness}
onChange={(e) => setNoteToBusiness(e.target.value)}
/>
</div>
<div>
<Label>Date</Label>
<DateTimePicker granularity="day" hourCycle={24} value={eventDate} onChange={setEventDate} />
</div>
<div>
) : !isLoadingMeetingLog || !isLoadingFreeDate ? (
<div className="space-y-4 w-[90%]">
<div>
<Label>Start Time</Label>
<TimePicker date={startTime} onChange={setStartTime} />
<Label htmlFor="eventName">Event Name</Label>
<Input
id="eventName"
placeholder="Enter event name"
value={eventName}
onChange={(e) => setEventName(e.target.value)}
/>
</div>
<div>
<Label>End Time</Label>
<TimePicker date={endTime} onChange={setEndTime} />
<Label htmlFor="eventDescription">Event Description</Label>
<Input
id="eventDescription"
placeholder="Enter event description"
value={eventDescription}
onChange={(e) => setEventDescription(e.target.value)}
/>
</div>
<div>
<Label htmlFor="eventDescription">Event Description</Label>
<Input
id="note"
placeholder="Your note to business"
value={noteToBusiness}
onChange={(e) => setNoteToBusiness(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-2">
<Label>Date</Label>
<Calendar
value={eventDate}
onChange={setEventDate}
minValue={today(getLocalTimeZone())}
isDateUnavailable={isDateUnavailable}
/>
</div>
<div>
<div>
<Label>Start Time</Label>
<TimeInput label="Start Time" value={startTime} onChange={setStartTime} />
</div>
<div>
<Label>End Time</Label>
<TimeInput label="End Time" value={endTime} onChange={setEndTime} />
</div>
</div>
</div>
</div>
) : (
<LegacyLoader />
)}
<DialogFooter className="sm:justify-start mt-4">
<Button type="button" onClick={handleCreateEvent} className="mr-2">
Create Event
<Button type="button" onClick={handleCreateEvent} className="mr-2" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Event"}
</Button>
<DialogClose asChild>
<Button type="button" variant="secondary">

View File

@ -1,4 +1,5 @@
import { Session } from "@supabase/supabase-js";
import { Database } from "@/types/database.types";
import { Session, SupabaseClient } from "@supabase/supabase-js";
export async function createCalendarEvent(
session: Session,
@ -33,9 +34,81 @@ export async function createCalendarEvent(
const data = await response.json();
console.log(data);
alert("Event created, check your Google Calendar!");
} catch (error) {
console.error("Error creating calendar event:", error);
alert("Failed to create the event.");
}
}
interface CreateMeetingLogProps {
client: SupabaseClient;
userId: string;
projectId: number;
meet_date: string;
start_time: string;
end_time: string;
note: string;
}
export async function createMeetingLog({
client,
userId,
projectId,
meet_date,
start_time,
end_time,
note,
}: CreateMeetingLogProps) {
const { error } = await client.from("meeting_log").insert([
{
meet_date: meet_date, // Format date as YYYY-MM-DD
start_time: start_time, // Format time as HH:MM:SS
end_time: end_time, // Format time as HH:MM:SS
note: note, // Text for meeting notes
user_id: userId, // Replace with a valid UUID
project_id: projectId,
},
]);
return error ? { status: false, error } : { status: true, error: null };
}
export function getMeetingLog(client: SupabaseClient<Database>, projectId: number) {
return client
.from("meeting_log")
.select(
`
id,
meet_date,
start_time,
end_time,
note,
user_id,
project_id,
created_at
`
)
.eq("project_id", projectId);
}
export function getFreeDate(client: SupabaseClient<Database>, projectId: number) {
return client.from("project_meeting_time").select("*").eq("project_id", projectId);
}
export async function specifyFreeDate({
client,
meet_date,
projectId,
}: {
client: SupabaseClient<Database>;
meet_date: string;
projectId: number;
}) {
const { error } = await client.from("project_meeting_time").insert([
{
project_id: projectId,
meet_date: meet_date, // Format date as YYYY-MM-DD
},
]);
return error ? { status: false, error } : { status: true, error: null };
}

View File

@ -0,0 +1,196 @@
import React, { useState } from "react";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Calendar, DateValue } from "@nextui-org/calendar";
import { specifyFreeDate, getFreeDate } from "../actions";
import toast from "react-hot-toast";
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
import { Label } from "@/components/ui/label";
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
import { LegacyLoader } from "@/components/loading/LegacyLoader";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "@/components/ui/alert-dialog";
interface DialogProps {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
// eslint-disable-next-line no-unused-vars
onOpenChange?(open: boolean): void;
modal?: boolean;
projectId: number;
}
export default function FreeTimeDialog(props: DialogProps) {
const supabase = createSupabaseClient();
const timezone = getLocalTimeZone();
const [selectedDate, setSelectedDate] = useState<CalendarDate | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [deleteDateId, setDeleteDateId] = useState<number | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState<boolean>(false);
const {
data: freeDate,
error: freeDateError,
isLoading: isLoadingFreeDate,
refetch: refetchFreeDate,
} = useQuery(getFreeDate(supabase, props.projectId), { enabled: !!props.projectId });
const handleSpecifyFreeDate = async () => {
if (!selectedDate) {
toast.error("Please select a date.");
return;
}
setIsSubmitting(true);
try {
const formattedDate = selectedDate.toString().split("T")[0];
const { status, error } = await specifyFreeDate({
client: supabase,
meet_date: formattedDate,
projectId: props.projectId,
});
if (!status || error) {
toast.error("Failed to save the free date. Please try again.");
return;
}
refetchFreeDate();
toast.success("Free time specified successfully!");
props.onOpenChange?.(false);
} catch (error) {
toast.error("There was an error specifying the free date. Please try again.");
} finally {
setIsSubmitting(false);
}
};
const handleDeleteFreeDate = async () => {
if (deleteDateId === null) return;
setIsSubmitting(true);
try {
const { error } = await supabase.from("project_meeting_time").delete().eq("id", deleteDateId);
if (error) {
toast.error("Failed to delete the free date. Please try again.");
return;
}
refetchFreeDate();
toast.success("Free date deleted successfully!");
} catch (error) {
toast.error("There was an error deleting the free date. Please try again.");
} finally {
setIsSubmitting(false);
setIsDeleteDialogOpen(false);
}
};
const meetingLogRanges = (freeDate || []).map((log) => {
const [year, month, day] = log.meet_date.split("-").map(Number);
const startDate = new CalendarDate(year, month, day);
const endDate = new CalendarDate(year, month, day);
return [startDate, endDate];
});
const disabledRanges = [...meetingLogRanges];
const isDateUnavailable = (date: DateValue) =>
disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0);
return (
<Dialog {...props}>
<DialogContent className="sm:max-w-md overflow-y-auto h-[80%]">
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">Specify Your Free Time</DialogTitle>
<DialogDescription>Select a date when you are available for a meeting with the investor.</DialogDescription>
</DialogHeader>
{freeDateError ? (
<div>Error Loading data</div>
) : isLoadingFreeDate ? (
<LegacyLoader />
) : (
<div className="flex flex-col space-y-4 w-[90%] mt-4">
<div className="flex flex-col space-y-2">
<Label>Date</Label>
<Calendar
value={selectedDate}
onChange={setSelectedDate}
minValue={today(timezone)}
isDateUnavailable={isDateUnavailable}
/>
</div>
<div className="flex flex-col space-y-2 mt-4">
<Label>Current Free Dates</Label>
<div className="p-2 border rounded-md space-y-2">
{freeDate && freeDate.length > 0 ? (
freeDate.map((date) => (
<div
key={date.id}
className="bg-gray-100 p-2 rounded cursor-pointer hover:bg-red-400"
onClick={() => {
setDeleteDateId(date.id);
setIsDeleteDialogOpen(true);
}}
>
{date.meet_date || "No date specified"}
</div>
))
) : (
<div>No free dates specified</div>
)}
</div>
</div>
</div>
)}
<DialogFooter className="sm:justify-start mt-4">
<Button onClick={handleSpecifyFreeDate} disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Free Date"}
</Button>
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the free date.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setIsDeleteDialogOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteFreeDate}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Dialog>
);
}

View File

@ -0,0 +1,101 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Clock } from "lucide-react";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { getMeetingLog } from "../actions";
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
import { LegacyLoader } from "@/components/loading/LegacyLoader";
interface DialogProps {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
// eslint-disable-next-line no-unused-vars
onOpenChange?(open: boolean): void;
modal?: boolean;
projectId: number;
}
export function ManageMeetDialog(props: DialogProps) {
const supabase = createSupabaseClient();
const {
data: meetingLog,
error: meetingLogError,
isLoading: isLoadingMeetingLog,
} = useQuery(getMeetingLog(supabase, props.projectId), {
enabled: !!props.projectId,
});
return (
<Dialog {...props}>
<DialogContent className="sm:max-w-md overflow-y-auto h-[80%]">
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
<Clock />
Meeting Request
</DialogTitle>
<DialogDescription>List of meeting you need to attend.</DialogDescription>
</DialogHeader>
{meetingLogError ? (
<div>Error Loading data</div>
) : isLoadingMeetingLog ? (
<LegacyLoader />
) : meetingLog && meetingLog.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Date</TableHead>
<TableHead>Start Time</TableHead>
<TableHead>End Time</TableHead>
<TableHead>User</TableHead>
<TableHead>Note</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{meetingLog.map((log) => (
<TableRow key={log.id}>
<TableCell className="font-medium">{log.meet_date}</TableCell>
<TableCell>{log.start_time}</TableCell>
<TableCell>{log.end_time}</TableCell>
<TableCell>{log.user_id}</TableCell>
<TableCell>{log.note || "No note provided"}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={5} className="text-right">
Total Meetings: {meetingLog.length}
</TableCell>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No meeting logs available</div>
)}
<DialogFooter className="sm:justify-start mt-4">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,74 @@
"use client";
import { useState } from "react";
import { Separator } from "@/components/ui/separator";
import { Card, CardContent, CardDescription, CardTitle, CardHeader } from "@/components/ui/card";
import { Tooltip, TooltipProvider, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ManageMeetDialog } from "./ManageMeetDialog";
import { Button } from "@/components/ui/button";
import FreeTimeDialog from "./FreeTimeDialog";
type ProjectCardSectionProps = {
id: number;
project_name: string;
project_short_description: string;
business_id: {
user_id: string;
};
dataroom_id: number | null;
};
type ProjectCardCalendarManageSectionProps = {
projectData: ProjectCardSectionProps[] | null;
};
export default function ProjectCardCalendarManageSection({ projectData }: ProjectCardCalendarManageSectionProps) {
const [showMeetModal, setShowMeetModal] = useState<boolean>(false);
const [showFreeTimeModal, setShowFreeTimeModal] = useState<boolean>(false);
const [currentProjectId, setCurrentProjectId] = useState<number | undefined>(undefined);
return (
<div id="content" className="grid grid-cols-2 space-x-2">
{projectData != null ? (
projectData.map((project) => (
<Card key={project.id} className="mb-3">
<CardHeader className="h-[55%]">
<CardTitle>{project.project_name}</CardTitle>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<CardDescription className="line-clamp-1">{project.project_short_description}</CardDescription>
</TooltipTrigger>
<TooltipContent>{project.project_short_description}</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<Separator className="mb-3" />
<CardContent className="flex gap-3">
<Button
onClick={() => {
setCurrentProjectId(project.id);
setTimeout(() => setShowMeetModal(true), 0);
}}
>
Meeting List
</Button>
<Button
onClick={() => {
setCurrentProjectId(project.id);
setTimeout(() => setShowFreeTimeModal(true), 0);
}}
>
Specify Free Time
</Button>
</CardContent>
{/* <CardFooter></CardFooter> */}
</Card>
))
) : (
<div>No project data</div>
)}
<ManageMeetDialog open={showMeetModal} onOpenChange={setShowMeetModal} projectId={currentProjectId!} />
<FreeTimeDialog open={showFreeTimeModal} onOpenChange={setShowFreeTimeModal} projectId={currentProjectId!} />
</div>
);
}

View File

@ -0,0 +1,62 @@
import { Separator } from "@/components/ui/separator";
import { Clock } from "lucide-react";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { getProjectByUserId } from "@/lib/data/projectQuery";
import { Suspense } from "react";
import { LegacyLoader } from "@/components/loading/LegacyLoader";
import { getUserRole } from "@/lib/data/userQuery";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import ProjectCardCalendarManageSection from "./ProjectCardSection";
export default async function ManageMeetingPage() {
const supabase = createSupabaseClient();
const { data: user, error: userError } = await supabase.auth.getUser();
if (userError) {
throw "Can't get user data!";
}
const userId = user.user?.id;
const { data: roleData, error: roleDataError } = await getUserRole(supabase, userId);
if (roleDataError) {
throw "Error fetching user data";
}
if (!roleData || roleData.role != "business") {
return (
<div className="container max-w-screen-xl">
<span className="flex gap-2 items-center mt-4">
<Clock />
<p className="text-2xl font-bold">Manage Meeting Request</p>
</span>
<Separator className="my-3" />
<div className="mb-3 mt-2">Please apply for business first to access functionalities of busienss account</div>
<Link href="/business/apply">
<Button>Apply for business</Button>
</Link>
</div>
);
}
const { data: projectData, error: projectDataError } = await getProjectByUserId(supabase, userId);
if (projectDataError) {
throw "Can't get project data";
}
return (
<div className="container max-w-screen-xl">
<span className="flex gap-2 items-center mt-4">
<Clock />
<p className="text-2xl font-bold">Manage Meeting Request</p>
</span>
<Separator className="my-3" />
<Suspense fallback={<LegacyLoader />}>
<ProjectCardCalendarManageSection projectData={projectData} />
</Suspense>
</div>
);
}

View File

@ -0,0 +1,36 @@
import { CalendarDate, getLocalTimeZone } from "@internationalized/date";
import { TimeValue } from "@react-types/datepicker";
export function isEventOverlapping(
eventDate: CalendarDate,
startTime: TimeValue,
endTime: TimeValue,
existingEvents: { meet_date: string; start_time: string; end_time: string }[]
): boolean {
const timezone = getLocalTimeZone();
const newStartDate = eventDate.toDate(timezone);
newStartDate.setHours(startTime.hour, startTime.minute);
const newEndDate = eventDate.toDate(timezone);
newEndDate.setHours(endTime.hour, endTime.minute);
for (const log of existingEvents) {
const [year, month, day] = log.meet_date.split("-").map(Number);
const existingStartDate = new Date(year, month - 1, day);
existingStartDate.setHours(parseInt(log.start_time.split(":")[0]), parseInt(log.start_time.split(":")[1]));
const existingEndDate = new Date(year, month - 1, day);
existingEndDate.setHours(parseInt(log.end_time.split(":")[0]), parseInt(log.end_time.split(":")[1]));
const isOverlapping =
(newStartDate < existingEndDate && newEndDate > existingStartDate) ||
(existingStartDate < newEndDate && existingEndDate > newStartDate);
if (isOverlapping) {
return true;
}
}
return false;
}

View File

@ -37,6 +37,7 @@ const DatetimePickerHourCycle = () => {
const supabase = createSupabaseClient();
const [showModal, setShowModal] = useState<boolean>(false);
const [currentProjectName, setCurrentProjectName] = useState<string>("");
const [currentProjectId, setCurrentProjectId] = useState<number | undefined>(undefined);
const { session, loading } = useSession();
const {
@ -125,7 +126,8 @@ const DatetimePickerHourCycle = () => {
<Button
onClick={() => {
setCurrentProjectName(projectInvestments[0].project_name);
setShowModal(true);
setCurrentProjectId(projectInvestments[0].project_id);
setTimeout(() => setShowModal(true), 0);
}}
>
Schedule Meeting
@ -136,9 +138,16 @@ const DatetimePickerHourCycle = () => {
</div>
<MeetEventDialog
open={showModal}
onOpenChange={setShowModal}
onOpenChange={(open) => {
setShowModal(open);
if (!open) {
setCurrentProjectId(undefined);
setCurrentProjectName("");
}
}}
session={session}
projectName={currentProjectName}
projectId={currentProjectId!}
/>
</div>
);

View File

@ -1,6 +1,5 @@
"use client";
import Image from "next/image";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Overview } from "@/components/ui/overview";
@ -16,6 +15,7 @@ import { overAllGraphData, fourYearGraphData, dayOftheWeekData } from "../portfo
import CountUp from "react-countup";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { Modal } from "@/components/modal";
export default function Dashboard() {
const supabase = createSupabaseClient();
@ -23,7 +23,13 @@ export default function Dashboard() {
const { session, loading: isLoadingSession } = useSession();
const userId = session?.user.id;
const [projects, setProjects] = useState<
{ id: number; project_name: string; business_id: { user_id: number }[]; dataroom_id: number }[]
{
id: number;
project_name: string;
project_short_description: string;
business_id: { user_id: string };
dataroom_id: number | null;
}[]
>([]);
const [latestInvestment, setLatestInvestment] = useState<
{
@ -50,17 +56,17 @@ export default function Dashboard() {
)
);
let graphData = [];
const filteredData = (investmentDetail?.data || []).filter((deal) => deal.project_id === currentProjectId);
const filteredProject = (investmentDetail?.data || []).filter((deal) => deal.project_id === currentProjectId);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
};
if (activeTab === "daily") {
graphData = dayOftheWeekData(filteredData);
graphData = dayOftheWeekData(filteredProject);
} else if (activeTab === "yearly") {
graphData = fourYearGraphData(filteredData);
graphData = fourYearGraphData(filteredProject);
} else {
graphData = overAllGraphData(filteredData);
graphData = overAllGraphData(filteredProject);
}
useEffect(() => {
@ -116,193 +122,191 @@ export default function Dashboard() {
return (
<div className="container max-w-screen-xl">
<Loader isSuccess={!isLoadingSession && !isLoadingProjects} />{" "}
<div className="md:hidden">
<Image
src="/examples/dashboard-light.png"
width={1280}
height={866}
alt="Dashboard"
className="block dark:hidden"
/>
<Image
src="/examples/dashboard-dark.png"
width={1280}
height={866}
alt="Dashboard"
className="hidden dark:block"
/>
</div>
<div className="hidden flex-col md:flex">
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
</div>
{projects && projects.length > 0 && (
<Tabs className="space-y-4" defaultValue={projects[0].project_name}>
<TabsList>
{projects.map((project) => (
<TabsTrigger
key={project.id}
value={project.project_name}
onClick={() => setCurrentProjectId(project.id)}
>
{project.project_name}
</TabsTrigger>
))}
</TabsList>
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
</div>
{projects && projects.length > 0 && (
<Tabs className="space-y-4" defaultValue={projects[0].project_name}>
<TabsList>
{projects.map((project) => (
<TabsContent value={project.project_name} className="space-y-4" key={project.id}>
<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>
<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="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
$
<CountUp
end={filteredData
.filter((project) => project.deal_status === "Completed")
.reduce((sum, current) => sum + current.deal_amount, 0)}
duration={1}
/>
</div>
</CardContent>
</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>
<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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
+<CountUp end={2350} duration={1} />
</div>
{/* <p className="text-xs text-muted-foreground">
<TabsTrigger
key={project.id}
value={project.project_name}
onClick={() => setCurrentProjectId(project.id)}
>
{project.project_name}
</TabsTrigger>
))}
</TabsList>
{projects.map((project) => (
<TabsContent value={project.project_name} className="space-y-4" key={project.id}>
<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>
<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="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
$
<CountUp
end={filteredProject
.filter((project) => project.deal_status === "Completed")
.reduce((sum, current) => sum + current.deal_amount, 0)}
duration={1}
/>
</div>
</CardContent>
</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>
<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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
+<CountUp end={2350} duration={1} />
</div>
{/* <p className="text-xs text-muted-foreground">
+180.1% from last month
</p> */}
</CardContent>
</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>
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<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" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
+<CountUp end={12234} duration={1} />
</div>
{/* <p className="text-xs text-muted-foreground">
</CardContent>
</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>
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<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" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
+<CountUp end={12234} duration={1} />
</div>
{/* <p className="text-xs text-muted-foreground">
+19% from last month
</p> */}
</CardContent>
</Card>
<Button
onClick={() => {
router.push(`/project/${project.id}/edit`);
}}
className="h-full bg-emerald-500 hover:bg-emerald-800 font-bold text-xl"
>
Edit Project
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 17.25V21h3.75l11.05-11.05-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 000-1.42l-2.34-2.34a1.003 1.003 0 00-1.42 0L15.13 4.5l3.75 3.75 1.83-1.21z"
fill="currentColor"
/>
</svg>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-56 grid-cols-3 ml-5">
{tabOptions.map((tab) => (
<TabsTrigger key={tab} value={tab}>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</TabsTrigger>
))}
</TabsList>
<CardContent className="pl-2 mt-5">
<Overview graphType={graphType} data={graphData} />
</CardContent>
</Card>
<Button
onClick={() => {
router.push(`/project/${project.id}/edit`);
}}
className="h-full bg-emerald-500 hover:bg-emerald-800 font-bold text-xl"
>
Edit Project
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 17.25V21h3.75l11.05-11.05-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 000-1.42l-2.34-2.34a1.003 1.003 0 00-1.42 0L15.13 4.5l3.75 3.75 1.83-1.21z"
fill="currentColor"
/>
</svg>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-56 grid-cols-3 ml-5">
{tabOptions.map((tab) => (
<TabsTrigger key={tab} value={tab}>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</TabsTrigger>
))}
</TabsList>
<CardContent className="pl-2 mt-5">
<Overview graphType={graphType} data={graphData} />
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
<TabsList>
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
Line
</TabsTrigger>
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
Bar
</TabsTrigger>
</TabsList>
</Tabs>
</CardContent>
</Tabs>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Funds</CardTitle>
</CardHeader>
<CardContent>
<RecentFunds
data={latestInvestment.map((item) => {
return {
name: item.username,
amount: item.dealAmount,
avatar: item.avatarUrl,
date: new Date(item.createdTime),
status: item.dealStatus,
profile_url: `/profile/${item.investorId}`,
};
})}
/>
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
<TabsList>
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
Line
</TabsTrigger>
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
Bar
</TabsTrigger>
</TabsList>
</Tabs>
</CardContent>
</Card>
</div>
</TabsContent>
))}
</Tabs>
)}
</div>
</Tabs>
</Card>
<Card className="col-span-4 md:col-span-3">
<CardHeader>
<CardTitle>Recent Funds</CardTitle>
</CardHeader>
<CardContent className="grid grid-flow-dense w-full">
<RecentFunds
data={latestInvestment.map((item) => {
return {
name: item.username,
amount: item.dealAmount,
avatar: item.avatarUrl,
date: new Date(item.createdTime),
status: item.dealStatus,
profile_url: `/profile/${item.investorId}`,
};
})}
/>
<div className="flex justify-center mt-5">
{filteredProject && filteredProject.length > 1 ? (
<Modal
data={filteredProject.map((item) => {
return {
date: item.created_time,
name: item.username,
amount: item.deal_amount,
status: item.deal_status,
logoURL: Array.isArray(item.avatar_url) ? item.avatar_url[0] : item.avatar_url,
profileURL: `/profile/${item.investor_id}`,
};
})}
/>
) : undefined}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
))}
</Tabs>
)}
</div>
</div>
);

View File

@ -1,5 +1,5 @@
import React from "react";
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Montserrat } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { ReactQueryClientProvider } from "@/components/ReactQueryClientProvider";
@ -22,6 +22,11 @@ export const metadata: Metadata = {
description: "B2DVentures is a financial services company.",
};
export const viewport: Viewport = {
initialScale: 1,
width: "device-width",
};
interface RootLayoutProps {
children: Readonly<React.ReactNode>;
}

View File

@ -24,6 +24,7 @@ import { NoDataAlert } from "@/components/alert/noData/alert";
import { error } from "console";
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
import Link from "next/link";
import { Modal } from "@/components/modal";
export default async function Portfolio({ params }: { params: { uid: string } }) {
const supabase = createSupabaseClient();
@ -53,6 +54,7 @@ export default async function Portfolio({ params }: { params: { uid: string } })
if (investorDealError) {
console.error(investorDealError);
}
const { data: localUser, error: localUserError } = await supabase.auth.getUser();
if (localUserError) {
console.error("Error while fetching user" + error);
@ -72,7 +74,16 @@ export default async function Portfolio({ params }: { params: { uid: string } })
const tags = deals ? await getInvestorProjectTag(supabase, deals) : [];
const latestDeals = deals
? await Promise.all(
(await getLatestInvestment(supabase, deals)).map(async (deal) => ({
(
await getLatestInvestment(
supabase,
deals.map((deal) => ({
...deal,
status: deal.deal_status,
project_id: deal.project_id,
}))
)
).map(async (deal) => ({
...deal,
logo_url: await deal.logo_url,
}))
@ -96,7 +107,6 @@ export default async function Portfolio({ params }: { params: { uid: string } })
<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">
@ -175,8 +185,8 @@ export default async function Portfolio({ params }: { params: { uid: string } })
</TabsContent>
</Tabs>
</div>
<div className="flex flex-cols-3 w-full gap-5 mt-5">
<Card className="w-1/3">
<div className="grid grid-cols-1 md:grid-cols-3 w-full gap-5 mt-5">
<Card className="w-full h-fit">
<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>
@ -201,7 +211,7 @@ export default async function Portfolio({ params }: { params: { uid: string } })
/>
</CardContent>
</Card>
<Card className="w-1/3">
<Card className="w-full h-fit">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-md font-bold">Types of Businesses Invested In</CardTitle>
<TooltipProvider>
@ -226,12 +236,39 @@ export default async function Portfolio({ params }: { params: { uid: string } })
/>
</CardContent>
</Card>
<Card className="w-1/3">
<Card className="w-full">
<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 className="mt-5 grid grid-flow-row-dense">
<RecentFunds
data={latestDeals.map((item) => {
return {
name: item.name,
amount: item.amount,
avatar: item.logo_url,
date: new Date(item.date),
status: item.status,
profile_url: `/deals/${item.projectId}`,
};
})}
/>
<div className="mt-5 flex justify-center">
{deals && deals.length > 5 ? (
<Modal
data={deals.map((item) => {
return {
date: item.created_time,
name: item.project_name,
amount: item.deal_amount,
status: item.deal_status,
logoURL: Array.isArray(item.avatar_url) ? item.avatar_url[0] : item.avatar_url,
profileURL: `/deals/${item.project_id}`,
};
})}
/>
) : undefined}
</div>
</CardContent>
</Card>
</div>

View File

@ -26,7 +26,7 @@ function getTotalInvestment(deals: { deal_amount: number }[]) {
}
async function getLatestInvestment(
supabase: SupabaseClient,
deals: { project_id: number; deal_amount: number; created_time: Date;}[]
deals: { project_id: number; deal_amount: number; created_time: Date; status: string }[]
) {
const llist = [];
const count = 5;
@ -43,6 +43,7 @@ async function getLatestInvestment(
amount: deals[i].deal_amount,
date: new Date(deals[i].created_time),
logo_url: url,
status: deals[i].status,
});
}

View File

@ -20,7 +20,7 @@ export function ProjectSection({ projectsData }: { projectsData: ProjectCardProp
}
return (
<div className="grid grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-4 gap-6">
{projectsData.map((project) => (
<div key={project.id}>
<Link href={`/deals/${project.id}`}>

View File

@ -1,5 +1,5 @@
"use client";
import { useEffect, useState, useMemo } from "react";
import { useEffect, useState, useMemo, useCallback } from "react";
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from "./ui/carousel";
import Image from "next/image";
@ -8,15 +8,40 @@ interface GalleryProps {
}
const Gallery = ({ images }: GalleryProps) => {
const [mainApi, setMainApi] = useState<CarouselApi>();
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi>();
const [mainApi, setMainApi] = useState<CarouselApi | null>(null);
const [thumbnailApi, setThumbnailApi] = useState<CarouselApi | null>(null);
const [current, setCurrent] = useState(0);
const [isReady, setIsReady] = useState(false);
const syncCarousels = useCallback(
(index: number) => {
if (mainApi && thumbnailApi) {
setCurrent(index);
mainApi.scrollTo(index);
thumbnailApi.scrollTo(index);
}
},
[mainApi, thumbnailApi]
);
const handleClick = useCallback(
(index: number) => {
syncCarousels(index);
},
[syncCarousels]
);
const mainImage = useMemo(
() =>
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" }} />
<Image
src={image.src}
alt={`Carousel Main Image ${index + 1}`}
fill
style={{ objectFit: "contain" }}
priority={index === 0}
/>
</CarouselItem>
)),
[images]
@ -25,61 +50,60 @@ const Gallery = ({ images }: GalleryProps) => {
const thumbnailImages = useMemo(
() =>
images.map((image, index) => (
<CarouselItem key={index} className="relative aspect-square basis-1/4" onClick={() => handleClick(index)}>
<CarouselItem
key={index}
className="relative aspect-square basis-1/4 cursor-pointer"
onClick={() => handleClick(index)}
>
<Image
className={`${index === current ? "border-2" : ""}`}
className={`transition-all duration-200 ${index === current ? "border-2 border-primary" : ""}`}
src={image.src}
fill
alt={`Carousel Thumbnail Image ${index + 1}`}
style={{ objectFit: "contain" }}
priority={index === 0}
/>
</CarouselItem>
)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[images, current]
[images, current, handleClick]
);
useEffect(() => {
if (!mainApi || !thumbnailApi) {
return;
}
if (!mainApi || !thumbnailApi) return;
if (isReady) return;
const handleTopSelect = () => {
const handleMainSelect = () => {
const selected = mainApi.selectedScrollSnap();
setCurrent(selected);
thumbnailApi.scrollTo(selected);
if (selected !== current) {
syncCarousels(selected);
}
};
const handleBottomSelect = () => {
const handleThumbnailSelect = () => {
const selected = thumbnailApi.selectedScrollSnap();
setCurrent(selected);
mainApi.scrollTo(selected);
if (selected !== current) {
syncCarousels(selected);
}
};
mainApi.on("select", handleTopSelect);
thumbnailApi.on("select", handleBottomSelect);
mainApi.on("select", handleMainSelect);
thumbnailApi.on("select", handleThumbnailSelect);
syncCarousels(0);
setIsReady(true);
return () => {
mainApi.off("select", handleTopSelect);
thumbnailApi.off("select", handleBottomSelect);
mainApi.off("select", handleMainSelect);
thumbnailApi.off("select", handleThumbnailSelect);
};
}, [mainApi, thumbnailApi]);
const handleClick = (index: number) => {
if (!mainApi || !thumbnailApi) {
return;
}
thumbnailApi.scrollTo(index);
mainApi.scrollTo(index);
setCurrent(index);
};
}, [mainApi, thumbnailApi, current, syncCarousels, isReady]);
return (
<div className="w-full max-w-xl sm:w-auto">
<Carousel setApi={setMainApi}>
<Carousel setApi={setMainApi} className="mb-2">
<CarouselContent className="m-1">{mainImage}</CarouselContent>
</Carousel>
<Carousel setApi={setThumbnailApi}>
<Carousel setApi={setThumbnailApi} className="cursor-pointer">
<CarouselContent className="m-1 h-16">{thumbnailImages}</CarouselContent>
</Carousel>
</div>

View File

@ -0,0 +1,293 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
export type ModalProps = {
date: Date;
amount: number;
name: string;
investorId?: string;
profileURL?: string;
logoURL?: string;
status?: string;
};
export const columns: ColumnDef<ModalProps>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
Name
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Button
className="bg-transparent hover:bg-transparent text-current"
onClick={() => {
window.location.href = row.getValue("profileURL");
}}
>
<Avatar className="h-9 w-9">
<AvatarImage src={row.getValue("logoURL")} />
<AvatarFallback>{(row.getValue("name") as string).slice(0, 2)}</AvatarFallback>
</Avatar>
<span className="ml-2">{row.getValue("name")}</span>
</Button>
</div>
),
},
{
accessorKey: "date",
header: () => <div className="text-left">Date</div>,
cell: ({ row }) => {
const formatted = new Date(row.getValue("date")).toUTCString();
return <div className=" font-medium">{formatted}</div>;
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<div className="flex items-center space-x-1">
<span className="relative flex h-3 w-3">
<span
className={`animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75 ${
row.getValue("status") === "In Progress"
? "bg-sky-400"
: row.getValue("status") === "Completed"
? "bg-green-400"
: "bg-yellow-400"
}`}
></span>
<span
className={`relative inline-flex rounded-full h-2 w-2 mt-[2px] ml-0.5 ${
row.getValue("status") === "In Progress"
? "bg-sky-500"
: row.getValue("status") === "Completed"
? "bg-green-500"
: "bg-yellow-500"
}`}
></span>
</span>
<p
className={`text-xs m-0 ${
row.getValue("status") === "In Progress"
? "text-sky-500"
: row.getValue("status") === "Completed"
? "text-green-500"
: "text-yellow-500"
}`}
></p>
<div
className={`capitalize ${
row.getValue("status") === "In Progress"
? "text-sky-500"
: row.getValue("status") === "Completed"
? "text-green-500"
: "text-yellow-500"
}`}
>
{row.getValue("status")}
</div>
</div>
),
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return <div className="text-right font-medium">{formatted}</div>;
},
},
{
accessorKey: "logoURL",
id: "logoURL",
header: () => null,
cell: () => null,
},
{
accessorKey: "profileURL",
id: "profileURL",
header: () => null,
cell: () => null,
},
];
export function DataTable({ data }: { data: ModalProps[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 5,
});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
return (
<div className="w-3/4 md:w-full">
<div className="flex items-center py-4">
<Input
placeholder="Filter names..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide() && column.id != "logoURL" && column.id != "profileURL")
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import { useState } from "react";
import { Menu, X } from "lucide-react";
import { Button } from "./ui/button";
import { motion } from "framer-motion";
export function MobileMenu() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<Button onClick={() => setIsVisible((prev) => !prev)}>
<Menu />
</Button>
{isVisible && (
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ duration: 0.3 }}
className="fixed top-0 left-0 w-full bg-gray-800 text-white"
>
<X className="cursor-pointer" onClick={() => setIsVisible(false)} />
</motion.div>
)}
</div>
);
}

29
src/components/modal.tsx Normal file
View File

@ -0,0 +1,29 @@
"use client";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "./ui/button";
import { DataTable } from "./dataTable";
export type ModalProps = {
date: Date;
amount: number;
name: string;
investorId?: string;
profileURL?: string;
logoURL?: string;
status?: string;
};
export function Modal({ data }: { data: ModalProps[] }) {
return (
<div>
<Dialog>
<DialogTrigger asChild>
<Button>View More</Button>
</DialogTrigger>
<DialogContent className="max-w-screen-md md:max-w-screen-lg ">
<DataTable data={data} />
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -34,9 +34,12 @@ export const AuthenticatedComponents = ({ uid, avatarUrl, notificationCount }: A
<div className="relative inline-block">
<Bell className="h-6 w-6 " />
{notificationCount >= 1 && (
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full animate-ping">
{notificationCount}
</span>
<div>
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full animate-ping"></span>
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-600 rounded-full ">
{notificationCount}
</span>
</div>
)}
</div>
</Link>

View File

@ -21,6 +21,8 @@ import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { getUserId } from "@/lib/supabase/actions/getUserId";
import { getUnreadNotificationCountByUserId } from "@/lib/data/notificationQuery";
import { MobileMenu } from "../mobileMenu";
const ListItem = React.forwardRef<React.ElementRef<"a">, React.ComponentPropsWithoutRef<"a">>(
({ className, title, children, ...props }, ref) => {
return (
@ -97,8 +99,10 @@ export async function NavigationBar() {
</span>
</Link>
</div>
<div className="flex items-center">
<div className="md:hidden">
<MobileMenu />
</div>
<div className="hidden md:flex items-center ">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
@ -146,7 +150,7 @@ export async function NavigationBar() {
</NavigationMenuList>
</NavigationMenu>
<div className="flex gap-2 pl-2">
<div className="hidden md:flex gap-2 pl-2">
<div className="mt-1">
<ThemeToggle />
</div>

View File

@ -115,14 +115,16 @@ const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
scrollNext,
canScrollPrev,
canScrollNext,
}}>
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}>
{...props}
>
{children}
</div>
</CarouselContext.Provider>
@ -183,7 +185,8 @@ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProp
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}>
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
@ -210,7 +213,8 @@ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<ty
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}>
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>

View File

@ -20,6 +20,9 @@ export const getInvestmentByProjectsIds = (client: SupabaseClient, projectIds: s
deal_status:value
),
project_id,
...project_id (
project_name
),
deal_amount,
created_time,
...profiles (
@ -56,7 +59,25 @@ export const getInvestmentByUserId = (client: SupabaseClient, userId: string) =>
export function getInvestorDeal(client: SupabaseClient, userId: string) {
return client
.from("investment_deal")
.select("*")
.select(
`
id,
...deal_status_id(
deal_status:value
),
project_id,
...project_id (
project_name
),
deal_amount,
created_time,
...profiles (
investor_id:id,
username,
avatar_url
)
`
)
.in("investor_id", [userId])
.order("created_time", { ascending: true });
}

View File

@ -1,5 +1,6 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { ProjectCardProps } from "@/types/ProjectCard";
import { Database } from "@/types/database.types";
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
try {
@ -205,7 +206,7 @@ const getProjectByBusinessId = (client: SupabaseClient, businessIds: string[]) =
.in("business_id", businessIds);
};
const getProjectByUserId = (client: SupabaseClient, userId: string) => {
const getProjectByUserId = (client: SupabaseClient<Database>, userId: string) => {
return client
.from("project")
.select(

Binary file not shown.

View File

@ -1,13 +1,15 @@
import {nextui} from '@nextui-org/theme';
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"./node_modules/@nextui-org/theme/dist/components/(calendar|date-input|button|ripple|spinner).js"
],
prefix: "",
theme: {
container: {
@ -74,7 +76,7 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate"), require('@tailwindcss/typography'),],
plugins: [require('tailwindcss-animate'),require('@tailwindcss/typography'),nextui()],
} satisfies Config
export default config