mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 21:44:06 +01:00
Merge branch 'dev' into front-end
This commit is contained in:
commit
3aac567695
@ -15,6 +15,7 @@ STRIPE_SECRET_KEY=stripe-secret-key
|
|||||||
|
|
||||||
# Testing Server URL
|
# Testing Server URL
|
||||||
NEXT_PUBLIC_TEST_URL=testing-server-url
|
NEXT_PUBLIC_TEST_URL=testing-server-url
|
||||||
|
BASE_URL = http://127.0.0.1:3000
|
||||||
|
|
||||||
# Admin User Credentials (Must exist in the real database)
|
# Admin User Credentials (Must exist in the real database)
|
||||||
NEXT_PUBLIC_ADMIN_EMAIL=admin@example.com
|
NEXT_PUBLIC_ADMIN_EMAIL=admin@example.com
|
||||||
|
|||||||
@ -15,6 +15,7 @@ STRIPE_SECRET_KEY=stripe-secret-key
|
|||||||
|
|
||||||
# Testing Server URL
|
# Testing Server URL
|
||||||
NEXT_PUBLIC_TEST_URL=testing-server-url
|
NEXT_PUBLIC_TEST_URL=testing-server-url
|
||||||
|
BASE_URL = http://127.0.0.1:3000
|
||||||
|
|
||||||
# Admin User Credentials (Must exist in the real database)
|
# Admin User Credentials (Must exist in the real database)
|
||||||
NEXT_PUBLIC_ADMIN_EMAIL=admin@example.com
|
NEXT_PUBLIC_ADMIN_EMAIL=admin@example.com
|
||||||
|
|||||||
2
.github/workflows/eslint.yml
vendored
2
.github/workflows/eslint.yml
vendored
@ -41,4 +41,4 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run eslint
|
- name: Run eslint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/tailwind.config.ts
|
||||||
1860
package-lock.json
generated
1860
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -12,6 +12,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@mdxeditor/editor": "^3.15.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-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
@ -32,8 +36,9 @@
|
|||||||
"@stripe/react-stripe-js": "^2.8.1",
|
"@stripe/react-stripe-js": "^2.8.1",
|
||||||
"@stripe/stripe-js": "^4.7.0",
|
"@stripe/stripe-js": "^4.7.0",
|
||||||
"@supabase-cache-helpers/postgrest-react-query": "^1.10.1",
|
"@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",
|
"@supabase/supabase-js": "^2.46.1",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tanstack/react-query": "^5.59.0",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-query-devtools": "^5.59.0",
|
"@tanstack/react-query-devtools": "^5.59.0",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
@ -42,7 +47,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.0",
|
"cmdk": "1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^3.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
"framer-motion": "^11.11.17",
|
"framer-motion": "^11.11.17",
|
||||||
@ -52,7 +57,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-countup": "^6.5.3",
|
"react-countup": "^6.5.3",
|
||||||
"react-day-picker": "^9",
|
"react-day-picker": "^9.*",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-file-icon": "^1.5.0",
|
"react-file-icon": "^1.5.0",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { UserIcon, UsersIcon } from "lucide-react";
|
import { Building2, ClipboardPen, Tag } from "lucide-react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getAllBusinessTypeQuery, getAllTagsQuery, getAllProjectStatusQuery } from "@/lib/data/dropdownQuery";
|
import { getAllBusinessTypeQuery, getAllTagsQuery, getAllProjectStatusQuery } from "@/lib/data/dropdownQuery";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
@ -140,7 +140,7 @@ export default function Deals() {
|
|||||||
{/* Business Type Filter */}
|
{/* Business Type Filter */}
|
||||||
<Select value={businessTypeFilter} onValueChange={(value) => setBusinessTypeFilter(value)}>
|
<Select value={businessTypeFilter} onValueChange={(value) => setBusinessTypeFilter(value)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<UsersIcon className="ml-2" />
|
<Building2 className="ml-2" />
|
||||||
<SelectValue placeholder="Business Type" />
|
<SelectValue placeholder="Business Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -169,7 +169,7 @@ export default function Deals() {
|
|||||||
{/* Project Status Filter */}
|
{/* Project Status Filter */}
|
||||||
<Select value={projectStatusFilter} onValueChange={(value) => setProjectStatusFilter(value)}>
|
<Select value={projectStatusFilter} onValueChange={(value) => setProjectStatusFilter(value)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<UserIcon className="ml-2" />
|
<ClipboardPen className="ml-2" />
|
||||||
<SelectValue placeholder="Project Status" />
|
<SelectValue placeholder="Project Status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -198,7 +198,7 @@ export default function Deals() {
|
|||||||
{/* Tags Filter */}
|
{/* Tags Filter */}
|
||||||
<Select value={tagFilter} onValueChange={(value) => setTagFilter(value)}>
|
<Select value={tagFilter} onValueChange={(value) => setTagFilter(value)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<UserIcon className="ml-2" />
|
<Tag className="ml-2" />
|
||||||
<SelectValue placeholder="Tags" />
|
<SelectValue placeholder="Tags" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@ -34,18 +34,20 @@ export const BusinessProfile = async ({ userId }: { userId: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl px-4">
|
<div className="container max-w-screen-xl px-4">
|
||||||
<Card className="mb-6 shadow-md rounded-lg bg-white">
|
<Card className="mb-6 shadow-md rounded-lg bg-white dark:bg-slate-900">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-center">
|
<div className="flex flex-col sm:flex-row justify-between items-center">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
<CardTitle className="text-2xl font-semibold">{data.business_name}</CardTitle>
|
<CardTitle className="text-2xl font-semibold">{data.business_name}</CardTitle>
|
||||||
<CardDescription className="text-md text-gray-600">{data.business_type}</CardDescription>
|
<CardDescription className="text-md text-gray-600 dark:text-gray-400">
|
||||||
|
{data.business_type}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 sm:mt-0">
|
<div className="mt-4 sm:mt-0">
|
||||||
<p className="text-lg text-gray-700">
|
<p className="text-lg text-gray-700 dark:text-gray-400">
|
||||||
<strong>Location:</strong> {data.location}
|
<strong>Location:</strong> {data.location}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-gray-700">
|
<p className="text-lg text-gray-700 dark:text-gray-400">
|
||||||
<strong>Joined on:</strong> {new Date(data.joined_date).toLocaleDateString()}
|
<strong>Joined on:</strong> {new Date(data.joined_date).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,16 +57,16 @@ export const BusinessProfile = async ({ userId }: { userId: string }) => {
|
|||||||
<CardContent className="py-3">
|
<CardContent className="py-3">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md font-semibold text-gray-800">Business ID:</p>
|
<p className="text-md font-semibold text-gray-800 dark:text-gray-500">Business ID:</p>
|
||||||
<p className="text-md text-gray-600">{data.business_id}</p>
|
<p className="text-md text-gray-600 dark:text-gray-400">{data.business_id}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md font-semibold text-gray-800">Business Type:</p>
|
<p className="text-md font-semibold text-gray-800 dark:text-gray-500">Business Type:</p>
|
||||||
<p className="text-md text-gray-600">{data.business_type}</p>
|
<p className="text-md text-gray-600 dark:text-gray-400">{data.business_type}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md font-semibold text-gray-800">User ID:</p>
|
<p className="text-md font-semibold text-gray-800 dark:text-gray-500">User ID:</p>
|
||||||
<p className="text-md text-gray-600">{data.user_id}</p>
|
<p className="text-md text-gray-600 dark:text-gray-400">{data.user_id}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -40,12 +40,12 @@ export const ProjectProfileSection = async ({ userId }: { userId: string }) => {
|
|||||||
<div className="overflow-y-auto max-h-screen flex flex-col gap-6">
|
<div className="overflow-y-auto max-h-screen flex flex-col gap-6">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
{data.map((project) => (
|
{data.map((project) => (
|
||||||
<Card key={project.id} className="shadow-lg rounded-lg bg-white overflow-hidden">
|
<Card key={project.id} className="shadow-lg rounded-lg bg-white dark:bg-slate-900 overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
<CardTitle className="text-2xl font-semibold">{project.project_name}</CardTitle>
|
<CardTitle className="text-2xl font-semibold">{project.project_name}</CardTitle>
|
||||||
<CardDescription className="text-md text-gray-600">
|
<CardDescription className="text-md text-gray-600 dark:text-gray-500">
|
||||||
{project.project_short_description}
|
{project.project_short_description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { SubmitHandler } from "react-hook-form";
|
import { SubmitHandler } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import BusinessForm from "./BusinessForm";
|
import BusinessForm from "./BusinessForm";
|
||||||
@ -8,9 +8,10 @@ import { businessFormSchema } from "@/types/schemas/application.schema";
|
|||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import { getCurrentUserID } from "@/app/api/userApi";
|
import { getCurrentUserID } from "@/app/api/userApi";
|
||||||
import { uploadFile } from "@/app/api/generalApi";
|
import { uploadFile } from "@/app/api/generalApi";
|
||||||
import { Loader } from "@/components/loading/loader";
|
// import { Loader } from "@/components/loading/loader";
|
||||||
import { hasUserApplied, transformChoice } from "./actions";
|
import { hasUserApplied, transformChoice } from "./actions";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
type businessSchema = z.infer<typeof businessFormSchema>;
|
type businessSchema = z.infer<typeof businessFormSchema>;
|
||||||
const BUCKET_PITCH_NAME = "business-application";
|
const BUCKET_PITCH_NAME = "business-application";
|
||||||
let supabase = createSupabaseClient();
|
let supabase = createSupabaseClient();
|
||||||
@ -18,14 +19,14 @@ let supabase = createSupabaseClient();
|
|||||||
export default function ApplyBusiness() {
|
export default function ApplyBusiness() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const alertShownRef = useRef(false);
|
const alertShownRef = useRef(false);
|
||||||
const [success, setSucess] = useState(false);
|
// const [success, setSucess] = useState(false);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
|
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
|
||||||
const transformedData = await transformChoice(data);
|
const transformedData = await transformChoice(data);
|
||||||
await sendApplication(transformedData);
|
await sendApplication(transformedData);
|
||||||
};
|
};
|
||||||
const sendApplication = async (recvData: any) => {
|
const sendApplication = async (recvData: any) => {
|
||||||
setSucess(false);
|
// setSucess(false);
|
||||||
const {
|
const {
|
||||||
data: { user },
|
data: { user },
|
||||||
} = await supabase.auth.getUser();
|
} = await supabase.auth.getUser();
|
||||||
@ -42,8 +43,8 @@ export default function ApplyBusiness() {
|
|||||||
if (!uploadSuccess) {
|
if (!uploadSuccess) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
toast.success("Send business appliction susccessfully!");
|
||||||
console.log("file upload successful");
|
router.push("/");
|
||||||
} else {
|
} else {
|
||||||
console.error("user ID is undefined.");
|
console.error("user ID is undefined.");
|
||||||
return;
|
return;
|
||||||
@ -67,7 +68,7 @@ export default function ApplyBusiness() {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
.select();
|
.select();
|
||||||
setSucess(true);
|
// setSucess(true);
|
||||||
// console.table(data);
|
// console.table(data);
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: error == null ? "success" : "error",
|
icon: error == null ? "success" : "error",
|
||||||
@ -82,11 +83,11 @@ export default function ApplyBusiness() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserData = async () => {
|
const fetchUserData = async () => {
|
||||||
try {
|
try {
|
||||||
setSucess(false);
|
// setSucess(false);
|
||||||
const userID = await getCurrentUserID();
|
const userID = await getCurrentUserID();
|
||||||
if (userID) {
|
if (userID) {
|
||||||
const hasApplied = await hasUserApplied(supabase, userID);
|
const hasApplied = await hasUserApplied(supabase, userID);
|
||||||
setSucess(true);
|
// setSucess(true);
|
||||||
if (hasApplied && !alertShownRef.current) {
|
if (hasApplied && !alertShownRef.current) {
|
||||||
alertShownRef.current = true;
|
alertShownRef.current = true;
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
@ -103,11 +104,11 @@ export default function ApplyBusiness() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setSucess(true);
|
// setSucess(true);
|
||||||
console.error("User ID is undefined.");
|
console.error("User ID is undefined.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSucess(true);
|
// setSucess(true);
|
||||||
console.error("Error fetching user ID:", error);
|
console.error("Error fetching user ID:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -118,7 +119,7 @@ export default function ApplyBusiness() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Loader isSuccess={success} />
|
{/* <Loader isSuccess={success} /> */}
|
||||||
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-100 dark:bg-gray-800 p-5">
|
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-100 dark:bg-gray-800 p-5">
|
||||||
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
|
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
|
||||||
Apply to raise on B2DVentures
|
Apply to raise on B2DVentures
|
||||||
|
|||||||
240
src/app/calendar/MeetEventDialog.tsx
Normal file
240
src/app/calendar/MeetEventDialog.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Clock } from "lucide-react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
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;
|
||||||
|
open?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
onOpenChange?(open: boolean): void;
|
||||||
|
modal?: boolean;
|
||||||
|
session: Session;
|
||||||
|
projectName: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MeetEventDialog(props: DialogProps) {
|
||||||
|
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) {
|
||||||
|
toast.error("Please fill in all event details.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
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 overflow-y-auto h-[80%]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex gap-2 items-center">
|
||||||
|
<Clock />
|
||||||
|
Arrange Meeting
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Arrange a meeting with the business you're interested in for more information.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{meetingLogError || freeDateError ? (
|
||||||
|
<div className="error-message">
|
||||||
|
<p>Error loading meeting logs</p>
|
||||||
|
</div>
|
||||||
|
) : !isLoadingMeetingLog || !isLoadingFreeDate ? (
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
</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 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>
|
||||||
|
) : (
|
||||||
|
<LegacyLoader />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="sm:justify-start mt-4">
|
||||||
|
<Button type="button" onClick={handleCreateEvent} className="mr-2" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Creating..." : "Create Event"}
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
src/app/calendar/actions.ts
Normal file
114
src/app/calendar/actions.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Database } from "@/types/database.types";
|
||||||
|
import { Session, SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export async function createCalendarEvent(
|
||||||
|
session: Session,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
eventName: string,
|
||||||
|
eventDescription: string
|
||||||
|
) {
|
||||||
|
console.log("Creating calendar event");
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
summary: eventName,
|
||||||
|
description: eventDescription,
|
||||||
|
start: {
|
||||||
|
dateTime: start.toISOString(),
|
||||||
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: end.toISOString(),
|
||||||
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + session.provider_token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating calendar event:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
196
src/app/calendar/manage/FreeTimeDialog.tsx
Normal file
196
src/app/calendar/manage/FreeTimeDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/app/calendar/manage/ManageMeetDialog.tsx
Normal file
101
src/app/calendar/manage/ManageMeetDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/app/calendar/manage/ProjectCardSection.tsx
Normal file
74
src/app/calendar/manage/ProjectCardSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
src/app/calendar/manage/page.tsx
Normal file
62
src/app/calendar/manage/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/app/calendar/overlapEvent.ts
Normal file
36
src/app/calendar/overlapEvent.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -1,21 +1,154 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { DateTimePicker } from "@/components/ui/datetime-picker";
|
import React, { useMemo, useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import useSession from "@/lib/supabase/useSession";
|
||||||
|
import { MeetEventDialog } from "./MeetEventDialog";
|
||||||
|
import { Clock } from "lucide-react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableFooter,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { getInvestmentByUserId } from "@/lib/data/investmentQuery";
|
||||||
|
import { LegacyLoader } from "@/components/loading/LegacyLoader";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
|
||||||
|
interface Investment {
|
||||||
|
id: any;
|
||||||
|
project_id: any;
|
||||||
|
project_name: any;
|
||||||
|
project_short_description: any;
|
||||||
|
dataroom_id: any;
|
||||||
|
deal_amount: any;
|
||||||
|
investor_id: any;
|
||||||
|
created_time: any;
|
||||||
|
}
|
||||||
|
|
||||||
const DatetimePickerHourCycle = () => {
|
const DatetimePickerHourCycle = () => {
|
||||||
const [date12, setDate12] = useState<Date | undefined>(undefined);
|
const supabase = createSupabaseClient();
|
||||||
const [date24, setDate24] = useState<Date | undefined>(undefined);
|
const [showModal, setShowModal] = useState<boolean>(false);
|
||||||
|
const [currentProjectName, setCurrentProjectName] = useState<string>("");
|
||||||
|
const [currentProjectId, setCurrentProjectId] = useState<number | undefined>(undefined);
|
||||||
|
const { session, loading } = useSession();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: investments = [],
|
||||||
|
error: investmentsError,
|
||||||
|
isLoading: isLoadingInvestments,
|
||||||
|
} = useQuery(getInvestmentByUserId(supabase, session?.user.id ?? ""), {
|
||||||
|
enabled: !!session?.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (investmentsError) {
|
||||||
|
throw "Error load investment data";
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectInvestments = useMemo(() => {
|
||||||
|
if (!investments) return {};
|
||||||
|
|
||||||
|
return investments.reduce((acc: { [key: string]: Investment[] }, investment: Investment) => {
|
||||||
|
const projectId = investment.project_id;
|
||||||
|
if (!acc[projectId]) {
|
||||||
|
acc[projectId] = [];
|
||||||
|
}
|
||||||
|
acc[projectId].push(investment);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}, [investments]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LegacyLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session || !session?.user.id) {
|
||||||
|
throw "Can't load session!";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingInvestments) {
|
||||||
|
return <LegacyLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 lg:flex-row">
|
<div className="container max-w-screen-xl">
|
||||||
<div className="flex w-72 flex-col gap-2">
|
<span className="flex gap-2 items-center mt-4">
|
||||||
<Label>12 Hour</Label>
|
<Clock />
|
||||||
<DateTimePicker hourCycle={12} value={date12} onChange={setDate12} />
|
<p className="text-2xl font-bold">Schedule Meeting</p>
|
||||||
</div>
|
</span>
|
||||||
<div className="flex w-72 flex-col gap-2">
|
<Separator className="my-3" />
|
||||||
<Label>24 Hour</Label>
|
<div className="space-y-2">
|
||||||
<DateTimePicker hourCycle={24} value={date24} onChange={setDate24} />
|
{Object.entries(projectInvestments).map(([projectId, projectInvestments]) => (
|
||||||
|
<Card key={projectId}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">{projectInvestments[0].project_name}</CardTitle>
|
||||||
|
<CardDescription>{projectInvestments[0].project_short_description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableCaption>Investments for this project</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Investment Id</TableHead>
|
||||||
|
<TableHead>Invested At</TableHead>
|
||||||
|
<TableHead className="text-right">Amount</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{projectInvestments.map((investment) => (
|
||||||
|
<TableRow key={investment.id}>
|
||||||
|
<TableCell className="font-medium">{investment.id}</TableCell>
|
||||||
|
<TableCell>{investment.created_time}</TableCell>
|
||||||
|
<TableCell className="text-right">{investment.deal_amount}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>Total</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{projectInvestments
|
||||||
|
.reduce((total, investment) => total + (parseFloat(investment.deal_amount) || 0), 0)
|
||||||
|
.toLocaleString("en-US", { style: "currency", currency: "USD" })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentProjectName(projectInvestments[0].project_name);
|
||||||
|
setCurrentProjectId(projectInvestments[0].project_id);
|
||||||
|
setTimeout(() => setShowModal(true), 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Schedule Meeting
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<MeetEventDialog
|
||||||
|
open={showModal}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowModal(open);
|
||||||
|
if (!open) {
|
||||||
|
setCurrentProjectId(undefined);
|
||||||
|
setCurrentProjectName("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
session={session}
|
||||||
|
projectName={currentProjectName}
|
||||||
|
projectId={currentProjectId!}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Overview } from "@/components/ui/overview";
|
import { Overview } from "@/components/ui/overview";
|
||||||
@ -16,7 +15,6 @@ import { overAllGraphData, fourYearGraphData, dayOftheWeekData } from "../portfo
|
|||||||
import CountUp from "react-countup";
|
import CountUp from "react-countup";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
|
||||||
import { Modal } from "@/components/modal";
|
import { Modal } from "@/components/modal";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
@ -25,7 +23,13 @@ export default function Dashboard() {
|
|||||||
const { session, loading: isLoadingSession } = useSession();
|
const { session, loading: isLoadingSession } = useSession();
|
||||||
const userId = session?.user.id;
|
const userId = session?.user.id;
|
||||||
const [projects, setProjects] = useState<
|
const [projects, setProjects] = useState<
|
||||||
{ id: number; project_name: string; business_id: { user_id: number }[]; dataroom_id: number }[]
|
{
|
||||||
|
id: number;
|
||||||
|
project_name: string;
|
||||||
|
project_short_description: string;
|
||||||
|
business_id: { user_id: string };
|
||||||
|
dataroom_id: number | null;
|
||||||
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const [latestInvestment, setLatestInvestment] = useState<
|
const [latestInvestment, setLatestInvestment] = useState<
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Montserrat } from "next/font/google";
|
import { Montserrat } from "next/font/google";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { ReactQueryClientProvider } from "@/components/ReactQueryClientProvider";
|
import { ReactQueryClientProvider } from "@/components/ReactQueryClientProvider";
|
||||||
@ -22,6 +22,11 @@ export const metadata: Metadata = {
|
|||||||
description: "B2DVentures is a financial services company.",
|
description: "B2DVentures is a financial services company.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
initialScale: 1,
|
||||||
|
width: "device-width",
|
||||||
|
};
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
children: Readonly<React.ReactNode>;
|
children: Readonly<React.ReactNode>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Loader } from "@/components/loading/loader";
|
import { LegacyLoader } from "@/components/loading/LegacyLoader";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return <Loader />;
|
return <LegacyLoader />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,6 @@ import { NoDataAlert } from "@/components/alert/noData/alert";
|
|||||||
import { error } from "console";
|
import { error } from "console";
|
||||||
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
|
import { UnAuthorizedAlert } from "@/components/alert/unauthorized/alert";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { DataTable } from "@/components/dataTable";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import CustomTooltip from "@/components/customToolTip";
|
|
||||||
import { Modal } from "@/components/modal";
|
import { Modal } from "@/components/modal";
|
||||||
|
|
||||||
export default async function Portfolio({ params }: { params: { uid: string } }) {
|
export default async function Portfolio({ params }: { params: { uid: string } }) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export function LoginButton(props: { nextUrl?: string }) {
|
|||||||
provider: "google",
|
provider: "google",
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${location.origin}/auth/callback?next=${props.nextUrl || ""}`,
|
redirectTo: `${location.origin}/auth/callback?next=${props.nextUrl || ""}`,
|
||||||
|
scopes: "https://www.googleapis.com/auth/calendar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export function SignupButton(props: { nextUrl?: string }) {
|
|||||||
provider: "google",
|
provider: "google",
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${location.origin}/auth/callback?next=${props.nextUrl || ""}`,
|
redirectTo: `${location.origin}/auth/callback?next=${props.nextUrl || ""}`,
|
||||||
|
scopes: "https://www.googleapis.com/auth/calendar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export type ModalProps = {
|
export type ModalProps = {
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Lottie from "react-lottie";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Dynamically import Lottie to prevent SSR issues
|
||||||
|
const Lottie = dynamic(() => import("react-lottie"), { ssr: false });
|
||||||
import * as loadingData from "./loading.json";
|
import * as loadingData from "./loading.json";
|
||||||
|
|
||||||
const loadingOption = {
|
const loadingOption = {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import { ProjectCardProps } from "@/types/ProjectCard";
|
import { ProjectCardProps } from "@/types/ProjectCard";
|
||||||
|
import { Database } from "@/types/database.types";
|
||||||
|
|
||||||
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
||||||
try {
|
try {
|
||||||
@ -205,7 +206,7 @@ const getProjectByBusinessId = (client: SupabaseClient, businessIds: string[]) =
|
|||||||
.in("business_id", businessIds);
|
.in("business_id", businessIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectByUserId = (client: SupabaseClient, userId: string) => {
|
const getProjectByUserId = (client: SupabaseClient<Database>, userId: string) => {
|
||||||
return client
|
return client
|
||||||
.from("project")
|
.from("project")
|
||||||
.select(
|
.select(
|
||||||
|
|||||||
Binary file not shown.
@ -1,13 +1,15 @@
|
|||||||
|
import {nextui} from '@nextui-org/theme';
|
||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
"./pages/**/*.{ts,tsx}",
|
||||||
'./components/**/*.{ts,tsx}',
|
"./components/**/*.{ts,tsx}",
|
||||||
'./app/**/*.{ts,tsx}',
|
"./app/**/*.{ts,tsx}",
|
||||||
'./src/**/*.{ts,tsx}',
|
"./src/**/*.{ts,tsx}",
|
||||||
],
|
"./node_modules/@nextui-org/theme/dist/components/(calendar|date-input|button|ripple|spinner).js"
|
||||||
|
],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
@ -74,7 +76,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate"), require('@tailwindcss/typography'),],
|
plugins: [require('tailwindcss-animate'),require('@tailwindcss/typography'),nextui()],
|
||||||
} satisfies Config
|
} satisfies Config
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { firefox, FullConfig } from '@playwright/test';
|
import { firefox, FullConfig } from '@playwright/test';
|
||||||
|
|
||||||
async function globalSetup(config: FullConfig) {
|
async function globalSetup(config: FullConfig) {
|
||||||
|
const { baseURL, storageState } = config.projects[0].use;
|
||||||
console.log('globalizing...');
|
console.log('globalizing...');
|
||||||
const email = process.env.NEXT_PUBLIC_TEST_USER_EMAIL;
|
const email = process.env.NEXT_PUBLIC_TEST_USER_EMAIL;
|
||||||
const password = process.env.NEXT_PUBLIC_TEST_USER_PASSWORD;
|
const password = process.env.NEXT_PUBLIC_TEST_USER_PASSWORD;
|
||||||
@ -13,12 +14,13 @@ async function globalSetup(config: FullConfig) {
|
|||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
console.log('signing up user...');
|
console.log('signing up user...');
|
||||||
await page.goto(config.projects[0].use.baseURL + '/auth/signup');
|
await page.goto(baseURL + '/auth/signup');
|
||||||
await page.fill('id=email', email);
|
await page.fill('id=email', email);
|
||||||
await page.fill('id=password', password);
|
await page.fill('id=password', password);
|
||||||
await page.fill('id=confirmPassword', password);
|
await page.fill('id=confirmPassword', password);
|
||||||
await page.click('id=signup')
|
await page.click('id=signup')
|
||||||
|
|
||||||
|
await page.context().storageState({ path: storageState as string });
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user