mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-19 05:54:06 +01:00
Merge branch 'main' into front-end
This commit is contained in:
commit
2db77b80a5
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
src/app/(investment)/deals/[id]/followShareButton.tsx
|
||||||
|
src/components/ui/*
|
||||||
|
src/types/database.types.ts
|
||||||
|
src/components/BusinessForm.tsx
|
||||||
|
src/components/ProjectForm.tsx
|
||||||
|
src/app/project/apply/page.tsx
|
||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals"
|
"extends": ["eslint:recommended", "next", "next/core-web-vitals"]
|
||||||
}
|
}
|
||||||
|
|||||||
44
.github/workflows/build.yml
vendored
Normal file
44
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Build CI
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Caching
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
${{ github.workspace }}/.next/cache
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_ID=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_ID }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_SECRET=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_SECRET }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_EMAIL=${{ secrets.NEXT_PUBLIC_DUMMY_EMAIL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_PASSWORD=${{ secrets.NEXT_PUBLIC_DUMMY_PASSWORD }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_STRIPE_PUBLIC_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL_SOURCE=${{ secrets.NEXT_PUBLIC_SUPABASE_URL_SOURCE }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_TEST_URL=${{ secrets.NEXT_PUBLIC_TEST_URL }} >> $GITHUB_ENV
|
||||||
|
echo PROJECT_ID=${{ secrets.PROJECT_ID }} >> $GITHUB_ENV
|
||||||
|
echo STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build --if-present
|
||||||
44
.github/workflows/eslint.yml
vendored
Normal file
44
.github/workflows/eslint.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Eslint CI
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Caching
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
${{ github.workspace }}/.next/cache
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_ID=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_ID }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_SECRET=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_SECRET }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_EMAIL=${{ secrets.NEXT_PUBLIC_DUMMY_EMAIL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_PASSWORD=${{ secrets.NEXT_PUBLIC_DUMMY_PASSWORD }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_STRIPE_PUBLIC_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL_SOURCE=${{ secrets.NEXT_PUBLIC_SUPABASE_URL_SOURCE }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_TEST_URL=${{ secrets.NEXT_PUBLIC_TEST_URL }} >> $GITHUB_ENV
|
||||||
|
echo PROJECT_ID=${{ secrets.PROJECT_ID }} >> $GITHUB_ENV
|
||||||
|
echo STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run eslint
|
||||||
|
run: npm run lint
|
||||||
81
.github/workflows/playwright.yml
vendored
81
.github/workflows/playwright.yml
vendored
@ -1,27 +1,54 @@
|
|||||||
# name: Playwright Tests
|
name: Playwright Tests
|
||||||
# on:
|
|
||||||
# push:
|
on: pull_request
|
||||||
# branches: [ main, master ]
|
|
||||||
# pull_request:
|
jobs:
|
||||||
# branches: [ main, master ]
|
build:
|
||||||
# jobs:
|
timeout-minutes: 10
|
||||||
# test:
|
runs-on: ubuntu-latest
|
||||||
# timeout-minutes: 60
|
|
||||||
# runs-on: ubuntu-latest
|
steps:
|
||||||
# steps:
|
- uses: actions/checkout@v4
|
||||||
# - uses: actions/checkout@v4
|
- uses: actions/setup-node@v4
|
||||||
# - uses: actions/setup-node@v4
|
with:
|
||||||
# with:
|
node-version: lts/*
|
||||||
# node-version: lts/*
|
|
||||||
# - name: Install dependencies
|
- name: Caching
|
||||||
# run: npm ci
|
uses: actions/cache@v4
|
||||||
# - name: Install Playwright Browsers
|
with:
|
||||||
# run: npx playwright install --with-deps
|
path: |
|
||||||
# - name: Run Playwright tests
|
~/.npm
|
||||||
# run: npx playwright test
|
${{ github.workspace }}/.next/cache
|
||||||
# - uses: actions/upload-artifact@v4
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||||
# if: ${{ !cancelled() }}
|
restore-keys: |
|
||||||
# with:
|
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||||
# name: playwright-report
|
|
||||||
# path: playwright-report/
|
- name: Set environment variables
|
||||||
# retention-days: 30
|
run: |
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_ID=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_ID }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_AUTH_GOOGLE_SECRET=${{ secrets.NEXT_PUBLIC_AUTH_GOOGLE_SECRET }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_EMAIL=${{ secrets.NEXT_PUBLIC_DUMMY_EMAIL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_DUMMY_PASSWORD=${{ secrets.NEXT_PUBLIC_DUMMY_PASSWORD }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_STRIPE_PUBLIC_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_SUPABASE_URL_SOURCE=${{ secrets.NEXT_PUBLIC_SUPABASE_URL_SOURCE }} >> $GITHUB_ENV
|
||||||
|
echo NEXT_PUBLIC_TEST_URL=${{ secrets.NEXT_PUBLIC_TEST_URL }} >> $GITHUB_ENV
|
||||||
|
echo PROJECT_ID=${{ secrets.PROJECT_ID }} >> $GITHUB_ENV
|
||||||
|
echo STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Run Playwright tests with 4 workers
|
||||||
|
run: npx playwright test --workers=4
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
[](https://github.com/Sosokker/B2D-Ventures/actions/workflows/build.yml)
|
||||||
|
[](https://github.com/Sosokker/B2D-Ventures/actions/workflows/eslint.yml)
|
||||||
|
[](https://github.com/Sosokker/B2D-Ventures/actions/workflows/playwright.yml)
|
||||||
# B2D-Ventures
|
# B2D-Ventures
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|||||||
2512
package-lock.json
generated
2512
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -6,12 +6,14 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@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-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
@ -30,7 +32,7 @@
|
|||||||
"@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.4.1",
|
||||||
"@supabase/supabase-js": "^2.45.2",
|
"@supabase/supabase-js": "^2.46.1",
|
||||||
"@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",
|
||||||
"b2d-ventures": "file:",
|
"b2d-ventures": "file:",
|
||||||
@ -47,35 +49,43 @@
|
|||||||
"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-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-file-icon": "^1.5.0",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-lottie": "^1.2.4",
|
"react-lottie": "^1.2.4",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"stripe": "^17.1.0",
|
"stripe": "^17.1.0",
|
||||||
"sweetalert2": "^11.14.3",
|
"sweetalert2": "^11.6.13",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/next": "^8.0.7",
|
"@types/next": "^8.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/react-fade-in": "^2.0.2",
|
"@types/react-fade-in": "^2.0.2",
|
||||||
|
"@types/react-file-icon": "^1.0.4",
|
||||||
"@types/react-lottie": "^1.2.10",
|
"@types/react-lottie": "^1.2.10",
|
||||||
"@types/react-select-country-list": "^2.2.3",
|
"@types/react-select-country-list": "^2.2.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"supabase": "^1.200.3",
|
"supabase": "^1.200.3",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4670
pnpm-lock.yaml
4670
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { ShareIcon, StarIcon } from "lucide-react";
|
import { ShareIcon, StarIcon } from "lucide-react";
|
||||||
|
|||||||
@ -5,52 +5,50 @@ import ReactMarkdown from "react-markdown";
|
|||||||
|
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
|
||||||
Carousel,
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
CarouselNext,
|
|
||||||
CarouselPrevious,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
import FollowShareButtons from "./followShareButton";
|
import FollowShareButtons from "./followShareButton";
|
||||||
|
|
||||||
import { getProjectData } from "@/lib/data/projectQuery";
|
import { getProjectData } from "@/lib/data/projectQuery";
|
||||||
|
import { getDealList } from "@/app/api/dealApi";
|
||||||
|
import { sumByKey, toPercentage } from "@/lib/utils";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function ProjectDealPage({
|
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: { id: number };
|
|
||||||
}) {
|
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
|
|
||||||
const { data: projectData, error: projectDataError } = await getProjectData(
|
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
|
||||||
supabase,
|
|
||||||
params.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const carouselData = [
|
if (!projectData) {
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
redirect("/deals");
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
}
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (projectDataError) {
|
if (projectDataError) {
|
||||||
console.error(projectDataError);
|
return (
|
||||||
return <div>Error while fetching project data</div>;
|
<div className="container max-w-screen-xl my-5">
|
||||||
|
<p className="text-red-600">Error fetching data. Please try again.</p>
|
||||||
|
<Button className="mt-4" onClick={() => window.location.reload()}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectBusinessOwnerId = projectData.user_id;
|
||||||
|
const dealList = await getDealList(projectBusinessOwnerId);
|
||||||
|
const totalDealAmount = sumByKey(dealList, "deal_amount");
|
||||||
|
// timeDiff, if negative convert to zero
|
||||||
|
const timeDiff = Math.max(new Date(projectData.investment_deadline).getTime() - new Date().getTime(), 0);
|
||||||
|
const hourLeft = Math.floor(timeDiff / (1000 * 60 * 60));
|
||||||
|
|
||||||
|
const carouselData = Array(5).fill({
|
||||||
|
src: projectData.card_image_url || "/boiler1.jpg",
|
||||||
|
alt: `${projectData.project_name} Image`,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl my-5">
|
<div className="container max-w-screen-xl my-5">
|
||||||
<div className="flex flex-col gap-y-10">
|
<div className="flex flex-col gap-y-10">
|
||||||
@ -59,29 +57,16 @@ export default async function ProjectDealPage({
|
|||||||
<div id="header" className="flex flex-col">
|
<div id="header" className="flex flex-col">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="flex">
|
<span className="flex">
|
||||||
<Image
|
<Image src="/logo.svg" alt="logo" width={50} height={50} className="sm:scale-75" />
|
||||||
src="/logo.svg"
|
<h1 className="mt-3 font-bold text-lg md:text-3xl">{projectData?.project_name}</h1>
|
||||||
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>
|
</span>
|
||||||
<FollowShareButtons />
|
<FollowShareButtons />
|
||||||
</div>
|
</div>
|
||||||
{/* end of pack */}
|
{/* end of pack */}
|
||||||
<p className="mt-2 sm:text-sm">
|
<p className="mt-2 sm:text-sm">{projectData?.project_short_description}</p>
|
||||||
{projectData?.project_short_description}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap mt-3">
|
<div className="flex flex-wrap mt-3">
|
||||||
{projectData?.tags.map((tag, index) => (
|
{projectData?.tags.map((tag, index) => (
|
||||||
<span
|
<span key={index} className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1">
|
||||||
key={index}
|
|
||||||
className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1"
|
|
||||||
>
|
|
||||||
{tag.tag_name}
|
{tag.tag_name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -89,21 +74,12 @@ export default async function ProjectDealPage({
|
|||||||
</div>
|
</div>
|
||||||
<div id="sub-content" className="flex flex-row mt-5">
|
<div id="sub-content" className="flex flex-row mt-5">
|
||||||
{/* image carousel */}
|
{/* image carousel */}
|
||||||
<div
|
<div id="image-corousel" className="shrink-0 w-[700px] flex flex-col">
|
||||||
id="image-corousel"
|
|
||||||
className="shrink-0 w-[700px] flex flex-col"
|
|
||||||
>
|
|
||||||
<Carousel className="w-full h-full ml-1">
|
<Carousel className="w-full h-full ml-1">
|
||||||
<CarouselContent className="flex h-full">
|
<CarouselContent className="flex h-full">
|
||||||
{carouselData.map((item, index) => (
|
{carouselData.map((item, index) => (
|
||||||
<CarouselItem key={index}>
|
<CarouselItem key={index}>
|
||||||
<Image
|
<Image src={item.src} alt={item.alt} width={700} height={400} className="rounded-lg" />
|
||||||
src={item.src}
|
|
||||||
alt={item.alt}
|
|
||||||
width={700}
|
|
||||||
height={400}
|
|
||||||
className="rounded-lg"
|
|
||||||
/>
|
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
))}
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
@ -115,13 +91,7 @@ export default async function ProjectDealPage({
|
|||||||
<CarouselContent className="flex space-x-1">
|
<CarouselContent className="flex space-x-1">
|
||||||
{carouselData.map((item, index) => (
|
{carouselData.map((item, index) => (
|
||||||
<CarouselItem key={index} className="flex">
|
<CarouselItem key={index} className="flex">
|
||||||
<Image
|
<Image src={item.src} alt={item.alt} width={200} height={100} className="rounded-lg basis-0" />
|
||||||
src={item.src}
|
|
||||||
alt={item.alt}
|
|
||||||
width={200}
|
|
||||||
height={100}
|
|
||||||
className="rounded-lg basis-0"
|
|
||||||
/>
|
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
))}
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
@ -130,39 +100,36 @@ export default async function ProjectDealPage({
|
|||||||
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
|
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
|
||||||
<div className="pl-5">
|
<div className="pl-5">
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8">
|
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
|
||||||
${projectData?.total_investment}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm md:text-lg">
|
<p className="text-sm md:text-lg">
|
||||||
{" "}
|
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
|
||||||
5% raised of $5M max goal
|
{projectData?.target_investment} max goal
|
||||||
</p>
|
</p>
|
||||||
<Progress
|
<Progress
|
||||||
value={
|
value={toPercentage(totalDealAmount, projectData?.target_investment)}
|
||||||
projectData?.total_investment /
|
|
||||||
projectData?.target_investment
|
|
||||||
}
|
|
||||||
className="w-[60%] h-3 mt-3"
|
className="w-[60%] h-3 mt-3"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-4xl md:mt-8">
|
<h1 className="font-semibold text-4xl md:mt-8">
|
||||||
<p className="text-xl md:text-4xl">
|
<p className="text-xl md:text-4xl">{dealList.length}</p>
|
||||||
{projectData?.total_investment}
|
|
||||||
</p>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm md:text-lg"> Investors</p>
|
<p className="text-sm md:text-lg">Investors</p>
|
||||||
</span>
|
</span>
|
||||||
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
||||||
<span>
|
<span>
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
|
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
|
||||||
<p className="text-xl md:text-4xl">1 hours</p>
|
{projectData?.investment_deadline ? (
|
||||||
<p> Left to invest</p>
|
<>
|
||||||
|
<p className="text-xl md:text-4xl">{Math.floor(hourLeft)} hours</p>
|
||||||
|
<p>Left to invest</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xl md:text-4xl">No deadline</p>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Button className="mt-5 w-3/4 h-12">
|
<Button className="mt-5 w-3/4 h-12">
|
||||||
<Link href={`/invest/${params.id}`}>
|
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
|
||||||
Invest in {projectData?.project_name}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -181,14 +148,13 @@ export default async function ProjectDealPage({
|
|||||||
<Tabs.Content value="pitch">
|
<Tabs.Content value="pitch">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle></CardTitle>
|
<CardTitle>{projectData.project_name}</CardTitle>
|
||||||
<CardDescription></CardDescription>
|
<CardDescription />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="prose prose-sm max-w-none">
|
<div className="prose prose-sm max-w-none">
|
||||||
<ReactMarkdown className="text-black dark:text-white">
|
<ReactMarkdown className="text-black dark:text-white">
|
||||||
{projectData?.project_description ||
|
{projectData?.project_description || "No pitch available."}
|
||||||
"No pitch available."}
|
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
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 { useEffect, useState } from "react";
|
||||||
import { Clock3Icon, UserIcon, UsersIcon } from "lucide-react";
|
import { Clock3Icon, UserIcon, UsersIcon } from "lucide-react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ProjectCard } from "@/components/projectCard";
|
import { ProjectCard } from "@/components/projectCard";
|
||||||
import { getAllTagsQuery, getALlFundedStatusQuery, getAllBusinessTypeQuery } from "@/lib/data/dropdownQuery";
|
import { getALlFundedStatusQuery, getAllBusinessTypeQuery } from "@/lib/data/dropdownQuery";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
import { searchProjectsQuery, FilterParams, FilterProjectQueryParams } from "@/lib/data/projectQuery";
|
import { searchProjectsQuery, FilterParams, FilterProjectQueryParams } from "@/lib/data/projectQuery";
|
||||||
@ -16,14 +16,23 @@ import Link from "next/link";
|
|||||||
interface Project {
|
interface Project {
|
||||||
project_id: string;
|
project_id: string;
|
||||||
project_name: string;
|
project_name: string;
|
||||||
project_short_description: string;
|
|
||||||
published_time: string;
|
published_time: string;
|
||||||
|
project_short_description: string;
|
||||||
card_image_url: string;
|
card_image_url: string;
|
||||||
business_location: string;
|
project_status: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
min_investment: number;
|
min_investment: number;
|
||||||
total_investment: number;
|
total_investment: number;
|
||||||
target_investment: number;
|
target_investment: number;
|
||||||
tags: { tag_name: string }[];
|
investment_deadline: string;
|
||||||
|
tags: {
|
||||||
|
tag_name: string;
|
||||||
|
}[];
|
||||||
|
business_type: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
business_location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectSection = ({ filteredProjects }: { filteredProjects: Project[] }) => {
|
const ProjectSection = ({ filteredProjects }: { filteredProjects: Project[] }) => {
|
||||||
@ -130,7 +139,7 @@ export default function Deals() {
|
|||||||
const [sortByTimeFilter, setSortByTimeFilter] = useState("all");
|
const [sortByTimeFilter, setSortByTimeFilter] = useState("all");
|
||||||
const [businessTypeFilter, setBusinessTypeFilter] = useState("all");
|
const [businessTypeFilter, setBusinessTypeFilter] = useState("all");
|
||||||
const [tagFilter, setTagFilter] = useState([]);
|
const [tagFilter, setTagFilter] = useState([]);
|
||||||
const [projectStatusFilter, setprojectStatusFilter] = useState("all");
|
const [projectStatusFilter, setProjectStatusFilter] = useState("all");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(4);
|
const [pageSize, setPageSize] = useState(4);
|
||||||
|
|
||||||
@ -143,16 +152,15 @@ export default function Deals() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterProjectQueryParams: FilterProjectQueryParams = {
|
const filterProjectQueryParams: FilterProjectQueryParams = {
|
||||||
searchTerm,
|
searchTerm: "",
|
||||||
tagsFilter: tagFilter,
|
tagsFilter: [],
|
||||||
projectStatusFilter,
|
projectStatusFilter: "all",
|
||||||
businessTypeFilter,
|
businessTypeFilter: "all",
|
||||||
sortByTimeFilter,
|
sortByTimeFilter: "all",
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: tags, isLoading: isLoadingTags, error: tagsLoadingError } = useQuery(getAllTagsQuery(supabase));
|
|
||||||
const {
|
const {
|
||||||
data: projectStatus,
|
data: projectStatus,
|
||||||
isLoading: isLoadingFunded,
|
isLoading: isLoadingFunded,
|
||||||
@ -173,11 +181,20 @@ export default function Deals() {
|
|||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setTagFilter([]);
|
setTagFilter([]);
|
||||||
setprojectStatusFilter("all");
|
setProjectStatusFilter("all");
|
||||||
setBusinessTypeFilter("all");
|
setBusinessTypeFilter("all");
|
||||||
setSortByTimeFilter("all");
|
setSortByTimeFilter("all");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePageSizeChange = (value: number) => {
|
||||||
|
setPageSize(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchTerm(searchTermVisual);
|
||||||
|
}, [searchTermVisual]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl mx-auto px-4">
|
<div className="container max-w-screen-xl mx-auto px-4">
|
||||||
<div className="h-auto mt-10">
|
<div className="h-auto mt-10">
|
||||||
@ -188,8 +205,7 @@ export default function Deals() {
|
|||||||
All companies are <u>vetted & pass due diligence.</u>
|
All companies are <u>vetted & pass due diligence.</u>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* {JSON.stringify(projects, null, 4)} */}
|
{/* Search Input and Filters */}
|
||||||
|
|
||||||
<div className="flex mt-10 gap-3">
|
<div className="flex mt-10 gap-3">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@ -218,7 +234,7 @@ export default function Deals() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Business Type Filter */}
|
{/* Business Type Filter */}
|
||||||
<Select onValueChange={(value) => setBusinessTypeFilter}>
|
<Select onValueChange={(value) => setBusinessTypeFilter(value)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<UsersIcon className="ml-2" />
|
<UsersIcon className="ml-2" />
|
||||||
<SelectValue placeholder="Business Type" />
|
<SelectValue placeholder="Business Type" />
|
||||||
@ -247,7 +263,7 @@ export default function Deals() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Project Status Filter */}
|
{/* Project Status Filter */}
|
||||||
<Select onValueChange={(key) => setprojectStatusFilter(key)}>
|
<Select onValueChange={(key) => setProjectStatusFilter(key)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<UserIcon className="ml-2" />
|
<UserIcon className="ml-2" />
|
||||||
<SelectValue placeholder="Project Status" />
|
<SelectValue placeholder="Project Status" />
|
||||||
@ -275,11 +291,54 @@ export default function Deals() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Active Filters */}
|
||||||
<ShowFilter filterParams={filterParams} clearAll={clearAll} />
|
<ShowFilter filterParams={filterParams} clearAll={clearAll} />
|
||||||
<Separator className="mt-10" />
|
<Separator className="mt-10" />
|
||||||
|
|
||||||
{/* Project Cards Section */}
|
{/* Project Cards Section */}
|
||||||
<ProjectSection filteredProjects={projects || []} />
|
{isLoadingProjects ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : projectsLoadingError ? (
|
||||||
|
<div>Error loading projects.</div>
|
||||||
|
) : projects ? (
|
||||||
|
<ProjectSection filteredProjects={projects} />
|
||||||
|
) : (
|
||||||
|
<div>No projects found!</div>
|
||||||
|
)}
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className="mt-6 flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPage((prevPage) => Math.max(prevPage - 1, 1))}
|
||||||
|
disabled={page === 1}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Page Size:</span>
|
||||||
|
<Select onValueChange={(value) => handlePageSizeChange(Number(value))}>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue placeholder={`${pageSize}`} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="4">4</SelectItem>
|
||||||
|
<SelectItem value="8">8</SelectItem>
|
||||||
|
<SelectItem value="12">12</SelectItem>
|
||||||
|
<SelectItem value="16">16</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPage((prevPage) => prevPage + 1)}
|
||||||
|
disabled={projects! && projects.length < pageSize}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useStripe, useElements, CardElement } from "@stripe/react-stripe-js";
|
import { useStripe, useElements, CardNumberElement, CardCvcElement, CardExpiryElement } from "@stripe/react-stripe-js";
|
||||||
import convertToSubcurrency from "@/lib/convertToSubcurrency";
|
import convertToSubcurrency from "@/lib/convertToSubcurrency";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -14,9 +14,9 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useSession from "@/lib/supabase/useSession";
|
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useRouter } from "next/navigation";
|
import Link from "next/link";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const CheckoutPage = ({
|
const CheckoutPage = ({
|
||||||
amount,
|
amount,
|
||||||
@ -35,11 +35,8 @@ const CheckoutPage = ({
|
|||||||
const [clientSecret, setClientSecret] = useState("");
|
const [clientSecret, setClientSecret] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); // New success dialog state
|
||||||
const isAcceptTerm = isAcceptTermAndService();
|
const isAcceptTerm = isAcceptTermAndService();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { session } = useSession();
|
|
||||||
const user = session?.user;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/create-payment-intent", {
|
fetch("/api/create-payment-intent", {
|
||||||
@ -72,7 +69,7 @@ const CheckoutPage = ({
|
|||||||
await stripe
|
await stripe
|
||||||
.confirmCardPayment(clientSecret, {
|
.confirmCardPayment(clientSecret, {
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: elements.getElement(CardElement)!,
|
card: elements.getElement(CardNumberElement)!,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
@ -90,12 +87,15 @@ const CheckoutPage = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
toast.error("Unexpected error with server");
|
||||||
console.error("Supabase Insert Error:", error.message);
|
console.error("Supabase Insert Error:", error.message);
|
||||||
} else {
|
} else {
|
||||||
console.log("Insert successful:", data);
|
console.log("Insert successful:", data);
|
||||||
router.push(`http://www.localhost:3000/payment-success?amount=${amount}`);
|
toast.success("Invest successfully");
|
||||||
|
setIsSuccessDialogOpen(true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
toast.error("Unexpected error with server");
|
||||||
console.error("Unexpected error during Supabase insert:", err);
|
console.error("Unexpected error during Supabase insert:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +108,8 @@ const CheckoutPage = ({
|
|||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-e-transparent align-[-0.125em] text-surface motion-reduce:animate-[spin_1.5s_linear_infinite] dark:text-white"
|
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-e-transparent align-[-0.125em] text-surface motion-reduce:animate-[spin_1.5s_linear_infinite] dark:text-white"
|
||||||
role="status">
|
role="status"
|
||||||
|
>
|
||||||
<span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">
|
<span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">
|
||||||
Loading...
|
Loading...
|
||||||
</span>
|
</span>
|
||||||
@ -119,7 +120,70 @@ const CheckoutPage = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{clientSecret && <CardElement />}
|
{clientSecret && (
|
||||||
|
<div className="space-y-4 bg-gray-50 p-6 rounded-lg shadow-md">
|
||||||
|
<div>
|
||||||
|
<label className="text-gray-700 text-sm font-medium" htmlFor="cardNumber">
|
||||||
|
Card Number
|
||||||
|
</label>
|
||||||
|
<CardNumberElement
|
||||||
|
id="cardNumber"
|
||||||
|
className="block w-full p-3 mt-2 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
options={{
|
||||||
|
style: {
|
||||||
|
base: {
|
||||||
|
fontSize: "16px",
|
||||||
|
color: "#32325d",
|
||||||
|
"::placeholder": { color: "#a0aec0" },
|
||||||
|
},
|
||||||
|
invalid: { color: "#e53e3e" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-gray-700 text-sm font-medium" htmlFor="cardExpiry">
|
||||||
|
Expiration Date
|
||||||
|
</label>
|
||||||
|
<CardExpiryElement
|
||||||
|
id="cardExpiry"
|
||||||
|
className="block w-full p-3 mt-2 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
options={{
|
||||||
|
style: {
|
||||||
|
base: {
|
||||||
|
fontSize: "16px",
|
||||||
|
color: "#32325d",
|
||||||
|
"::placeholder": { color: "#a0aec0" },
|
||||||
|
},
|
||||||
|
invalid: { color: "#e53e3e" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-gray-700 text-sm font-medium" htmlFor="cardCvc">
|
||||||
|
CVC
|
||||||
|
</label>
|
||||||
|
<CardCvcElement
|
||||||
|
id="cardCvc"
|
||||||
|
className="block w-full p-3 mt-2 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
options={{
|
||||||
|
style: {
|
||||||
|
base: {
|
||||||
|
fontSize: "16px",
|
||||||
|
color: "#32325d",
|
||||||
|
"::placeholder": { color: "#a0aec0" },
|
||||||
|
},
|
||||||
|
invalid: { color: "#e53e3e" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{errorMessage && <div>{errorMessage}</div>}
|
{errorMessage && <div>{errorMessage}</div>}
|
||||||
|
|
||||||
@ -130,7 +194,8 @@ const CheckoutPage = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsDialogOpen(true)}
|
onClick={() => setIsDialogOpen(true)}
|
||||||
disabled={!stripe || loading}
|
disabled={!stripe || loading}
|
||||||
className="text-white w-full p-5 bg-black mt-2 rounded-md font-bold disabled:opacity-50 disabled:animate-pulse">
|
className="text-white w-full p-5 bg-black mt-2 rounded-md font-bold disabled:opacity-50 disabled:animate-pulse"
|
||||||
|
>
|
||||||
{!loading ? `Pay $${amount}` : "Processing..."}
|
{!loading ? `Pay $${amount}` : "Processing..."}
|
||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -154,6 +219,23 @@ const CheckoutPage = ({
|
|||||||
{errorMessage && <p className="text-red-500 mt-2 text-lg font-bold">{errorMessage}</p>}
|
{errorMessage && <p className="text-red-500 mt-2 text-lg font-bold">{errorMessage}</p>}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Success Dialog */}
|
||||||
|
<Dialog open={isSuccessDialogOpen} onOpenChange={setIsSuccessDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Thank you!</DialogTitle>
|
||||||
|
<DialogDescription>You successfully sent ${amount}.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button">
|
||||||
|
<Link href="/">Return to Main Page</Link>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -15,7 +14,6 @@ import { loadStripe } from "@stripe/stripe-js";
|
|||||||
|
|
||||||
import { getProjectDataQuery } from "@/lib/data/projectQuery";
|
import { getProjectDataQuery } from "@/lib/data/projectQuery";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import useSession from "@/lib/supabase/useSession";
|
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY === undefined) {
|
if (process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY === undefined) {
|
||||||
throw new Error("NEXT_PUBLIC_STRIPE_PUBLIC_KEY is not defined");
|
throw new Error("NEXT_PUBLIC_STRIPE_PUBLIC_KEY is not defined");
|
||||||
@ -72,7 +70,11 @@ export default function InvestPage() {
|
|||||||
fetchInvestorData();
|
fetchInvestorData();
|
||||||
}, [supabase]);
|
}, [supabase]);
|
||||||
|
|
||||||
const { data: projectData, isLoading: isLoadingProject } = useQuery(getProjectDataQuery(supabase, Number(params.id)));
|
const {
|
||||||
|
data: projectData,
|
||||||
|
isLoading: isLoadingProject,
|
||||||
|
error: projectError,
|
||||||
|
} = useQuery(getProjectDataQuery(supabase, Number(params.id)));
|
||||||
|
|
||||||
const handleCheckboxChange = (index: number) => {
|
const handleCheckboxChange = (index: number) => {
|
||||||
const updatedCheckedTerms = [...checkedTerms];
|
const updatedCheckedTerms = [...checkedTerms];
|
||||||
@ -89,57 +91,69 @@ export default function InvestPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-10 md:mx-40 my-10">
|
<div className="mx-10 md:mx-40 my-10">
|
||||||
<h1 className="text-2xl md:text-4xl font-bold">Invest on ${projectData?.project_name}</h1>
|
{isLoadingProject ? (
|
||||||
<Separator className="my-4" />
|
<p>Loading project details...</p>
|
||||||
<div></div>
|
) : projectError ? (
|
||||||
<div>
|
<p>Error loading project data. Please try again later.</p>
|
||||||
<div className="w-1/2 space-y-2">
|
) : projectData ? (
|
||||||
<h2 className="text:base md:text-2xl">Investment Amount</h2>
|
<>
|
||||||
<Input
|
<h1 className="text-2xl md:text-4xl font-bold">Invest in {projectData.project_name}</h1>
|
||||||
className="w-52"
|
<Separator className="my-4" />
|
||||||
type="number"
|
|
||||||
placeholder="min $10"
|
|
||||||
min={10}
|
|
||||||
onChangeCapture={(e) => setInvestAmount(Number(e.currentTarget.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-4" />
|
|
||||||
|
|
||||||
<div className=" md:w-2/3 space-y-2">
|
{/* Investment Amount Section */}
|
||||||
<h2 className="text-2xl">Terms and Services</h2>
|
<div className="w-1/2 space-y-2">
|
||||||
<Table>
|
<h2 className="text:base md:text-2xl">Investment Amount</h2>
|
||||||
<TableHeader>
|
<Input
|
||||||
<TableRow>
|
className="w-52"
|
||||||
<TableHead>Select</TableHead>
|
type="number"
|
||||||
<TableHead>Term</TableHead>
|
placeholder="min $10"
|
||||||
<TableHead>Description</TableHead>
|
min={10}
|
||||||
</TableRow>
|
onChangeCapture={(e) => setInvestAmount(Number(e.currentTarget.value))}
|
||||||
</TableHeader>
|
/>
|
||||||
<TableBody>
|
</div>
|
||||||
{term_data.map((item, index) => (
|
<Separator className="my-4" />
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>
|
{/* Terms and Services Section */}
|
||||||
<input type="checkbox" checked={checkedTerms[index]} onChange={() => handleCheckboxChange(index)} />
|
<div className="md:w-2/3 space-y-2">
|
||||||
</TableCell>
|
<h2 className="text-2xl">Terms and Services</h2>
|
||||||
<TableCell>{item.term}</TableCell>
|
<Table>
|
||||||
<TableCell>{item.description}</TableCell>
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Select</TableHead>
|
||||||
|
<TableHead>Term</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{term_data.map((item, index) => (
|
||||||
</div>
|
<TableRow key={index}>
|
||||||
<Separator className="my-4" />
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checkedTerms[index]}
|
||||||
|
onChange={() => handleCheckboxChange(index)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{item.term}</TableCell>
|
||||||
|
<TableCell>{item.description}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
|
||||||
<div className="w-full space-y-2">
|
{/* Payment Information Section */}
|
||||||
<h2 className="text:base md:text-2xl">Payment Information</h2>
|
<div className="w-full space-y-2">
|
||||||
<div>
|
<h2 className="text:base md:text-2xl">Payment Information</h2>
|
||||||
<Elements
|
<Elements
|
||||||
stripe={stripePromise}
|
stripe={stripePromise}
|
||||||
options={{
|
options={{
|
||||||
mode: "payment",
|
mode: "payment",
|
||||||
amount: convertToSubcurrency(investAmount),
|
amount: convertToSubcurrency(investAmount),
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<CheckoutPage
|
<CheckoutPage
|
||||||
amount={investAmount}
|
amount={investAmount}
|
||||||
isAcceptTermAndService={isAcceptTermAndService}
|
isAcceptTermAndService={isAcceptTermAndService}
|
||||||
@ -148,8 +162,10 @@ export default function InvestPage() {
|
|||||||
/>
|
/>
|
||||||
</Elements>
|
</Elements>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
) : (
|
||||||
|
<p>No project data found.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,36 +5,34 @@ import { BellIcon } from "lucide-react";
|
|||||||
|
|
||||||
export default function Notification() {
|
export default function Notification() {
|
||||||
const sampleNotifications = [
|
const sampleNotifications = [
|
||||||
{ message: "New message from John Doe", time: "5 minutes ago" },
|
{ id: 1, message: "New message from John Doe", time: "5 minutes ago" },
|
||||||
{ message: "Your order has been shipped", time: "2 hours ago" },
|
{ id: 2, message: "Your order has been shipped", time: "2 hours ago" },
|
||||||
{ message: "Meeting reminder: Team sync at 3 PM", time: "1 day ago" },
|
{ id: 3, message: "Meeting reminder: Team sync at 3 PM", time: "1 day ago" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="ml-24 md:ml-56 mt-16 ">
|
<div className="ml-24 md:ml-56 mt-16 ">
|
||||||
<h1 className="font-bold text-2xl md:text-3xl h-0">Notifications</h1>
|
<h1 className="font-bold text-2xl md:text-3xl h-0">Notifications</h1>
|
||||||
<div className=" w-full mt-20 ">
|
<div className="w-full mt-20">
|
||||||
{/* Cards */}
|
{/* Cards */}
|
||||||
<Card className=" border-slate-800 w-3/4 p-6">
|
<Card className="border-slate-800 w-3/4 p-6">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{sampleNotifications.map((notification, _) => (
|
{sampleNotifications.map((notification) => (
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className="flex items-center justify-between p-4 border-b border-gray-200"
|
||||||
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<BellIcon className="w-5 h-5 text-blue-500 mr-3" />
|
<BellIcon className="w-5 h-5 text-blue-500 mr-3" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium ">
|
<p className="text-sm font-medium">{notification.message}</p>
|
||||||
{notification.message}
|
<p className="text-xs text-gray-500">{notification.time}</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{notification.time}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-sm text-blue-500 hover:text-blue-600">
|
<button className="text-sm text-blue-500 hover:text-blue-600">Mark as read</button>
|
||||||
Mark as read
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
149
src/app/(user)/profile/[uid]/edit/page.tsx
Normal file
149
src/app/(user)/profile/[uid]/edit/page.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { updateProfile } from "@/lib/data/profileMutate";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "@/components/ui/form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { profileSchema } from "@/types/schemas/profile.schema";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { uploadAvatar } from "@/lib/data/bucket/uploadAvatar";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import useSession from "@/lib/supabase/useSession";
|
||||||
|
|
||||||
|
export default function EditProfilePage({ params }: { params: { uid: string } }) {
|
||||||
|
const uid = params.uid;
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const router = useRouter();
|
||||||
|
const { session, loading: isLoadingSession } = useSession();
|
||||||
|
|
||||||
|
const profileForm = useForm<z.infer<typeof profileSchema>>({
|
||||||
|
resolver: zodResolver(profileSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoadingSession) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<p>Loading session...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProfileSubmit = async (updates: z.infer<typeof profileSchema>) => {
|
||||||
|
const { avatars, username, full_name, bio } = updates;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let avatarUrl = null;
|
||||||
|
|
||||||
|
if (avatars instanceof File) {
|
||||||
|
const avatarData = await uploadAvatar(client, avatars, uid);
|
||||||
|
avatarUrl = avatarData?.path
|
||||||
|
? `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${avatarData.path}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateProfile(client, uid, {
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
bio,
|
||||||
|
...(avatarUrl && { avatar_url: avatarUrl }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
toast.success("Profile updated successfully!");
|
||||||
|
router.push(`/profile/${uid}`);
|
||||||
|
} else {
|
||||||
|
toast.error("No fields to update!");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error updating profile!");
|
||||||
|
console.error("Error updating profile:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uid != session?.user.id) {
|
||||||
|
router.push(`/profile/${uid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl">
|
||||||
|
<div className="my-5">
|
||||||
|
<span className="text-2xl font-bold">Update Profile</span>
|
||||||
|
<Separator className="my-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Form {...profileForm}>
|
||||||
|
<form onSubmit={profileForm.handleSubmit(onProfileSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={profileForm.control}
|
||||||
|
name="avatars"
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
render={({ field: { value, onChange, ...fieldProps } }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Avatar</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...fieldProps}
|
||||||
|
type="file"
|
||||||
|
onChange={(event) => onChange(event.target.files && event.target.files[0])}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription />
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={profileForm.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>This is your public display name.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={profileForm.control}
|
||||||
|
name="full_name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Full Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>This is your public full name.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={profileForm.control}
|
||||||
|
name="bio"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bio</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea placeholder="Your bio here" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>This is your public bio description.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,15 +2,18 @@ import React from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
import { getUserProfile } from "@/lib/data/userQuery";
|
import { getUserProfile } from "@/lib/data/userQuery";
|
||||||
import { Tables } from "@/types/database.types";
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import Link from "next/link";
|
||||||
interface Profile extends Tables<"Profiles"> {}
|
import { getUserRole } from "@/lib/data/userQuery";
|
||||||
|
|
||||||
export default async function ProfilePage({ params }: { params: { uid: string } }) {
|
export default async function ProfilePage({ params }: { params: { uid: string } }) {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
const uid = params.uid;
|
const uid = params.uid;
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
const { data: userRoleData, error: userRoleError } = await getUserRole(supabase, uid);
|
||||||
|
|
||||||
const { data: profileData, error } = await getUserProfile(supabase, uid);
|
const { data: profileData, error } = await getUserProfile(supabase, uid);
|
||||||
|
|
||||||
@ -22,6 +25,14 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userRoleError) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<p className="text-red-500">Error fetching role data. Please try again later.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!profileData) {
|
if (!profileData) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen">
|
||||||
@ -33,11 +44,13 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
|||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl px-4 py-8">
|
<div className="container max-w-screen-xl px-4 py-8">
|
||||||
<div className="bg-card border-2 border-border shadow-xl rounded-lg overflow-hidden">
|
<div className="bg-card border-2 border-border shadow-xl rounded-lg overflow-hidden">
|
||||||
<div className="bg-cover bg-center h-64 p-4" style={{ backgroundImage: "url(./banner.jpg)" }}>
|
<div className="bg-cover bg-center h-64 p-4" style={{ backgroundImage: "url(/banner.jpg)" }}>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
{user && user.id === uid && (
|
||||||
Edit Profile
|
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||||
</button>
|
<Link href={`/profile/${uid}/edit`}>Edit Profile</Link>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
@ -64,13 +77,19 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* Business Profile Indicator */}
|
||||||
|
{userRoleData.role === "business" && (
|
||||||
|
<span className="mt-2 inline-block bg-yellow-500 text-white text-sm font-medium px-2 py-1 rounded-full">
|
||||||
|
Business Profile
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Lower */}
|
{/* Lower */}
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h2 className="text-xl font-semibold mb-2">Bio</h2>
|
<h2 className="text-xl font-semibold mb-2">Bio</h2>
|
||||||
<div className="prose prose-sm max-w-none">
|
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||||
<ReactMarkdown>{profileData.bio || "No bio available."}</ReactMarkdown>
|
<ReactMarkdown>{profileData.bio || "No bio available."}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
59
src/app/admin/BusinessTable.tsx
Normal file
59
src/app/admin/BusinessTable.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Table, TableHeader, TableRow, TableCell, TableBody, TableHead } from "@/components/ui/table";
|
||||||
|
|
||||||
|
interface Business {
|
||||||
|
id: any;
|
||||||
|
location: any;
|
||||||
|
business_name: any;
|
||||||
|
business_type: any;
|
||||||
|
joined_date: any;
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
full_name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BusinessTableProps {
|
||||||
|
businesses: Business[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BusinessTable = ({ businesses }: BusinessTableProps) => {
|
||||||
|
if (!businesses) return <div>No data available</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table className="min-w-full border-2 border-border">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Business Name</TableHead>
|
||||||
|
<TableHead>Owner</TableHead>
|
||||||
|
<TableHead>Business Type</TableHead>
|
||||||
|
<TableHead>Location</TableHead>
|
||||||
|
<TableHead>Joined Date</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{businesses.map((business) => (
|
||||||
|
<TableRow key={business.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Link href={`/admin/business/${business.id}/projects`}>
|
||||||
|
<p className="hover:text-blue-600">{business.business_name}</p>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link href={`/profile/${business.user_id}`}>
|
||||||
|
<p className="hover:text-blue-600">{business.username}</p>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{business.business_type}</TableCell>
|
||||||
|
<TableCell>{business.location}</TableCell>
|
||||||
|
<TableCell>{new Date(business.joined_date).toLocaleDateString()}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessTable;
|
||||||
104
src/app/admin/business/BusinessActionButtons.tsx
Normal file
104
src/app/admin/business/BusinessActionButtons.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Check, X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { rejectBusiness, approveBusiness } from "@/lib/data/applicationMutate";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface BusinessActionButtonsProps {
|
||||||
|
businessApplicationId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BusinessActionButtons({ businessApplicationId }: BusinessActionButtonsProps) {
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectOpen, setIsRejectOpen] = useState(false);
|
||||||
|
const [isApproveOpen, setIsApproveOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRejectBusiness = async () => {
|
||||||
|
try {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
|
||||||
|
const { error } = await rejectBusiness(client, businessApplicationId);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast.success("Rejected successfully");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to reject business");
|
||||||
|
console.error("Failed to reject business:", error);
|
||||||
|
} finally {
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
setIsRejectOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onApproveBusiness = async () => {
|
||||||
|
try {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
|
||||||
|
const { error } = await approveBusiness(client, businessApplicationId);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast.success("Approved successfully");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to approve business");
|
||||||
|
console.error("Failed to approve business:", error);
|
||||||
|
} finally {
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
setIsApproveOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Dialog open={isApproveOpen} onOpenChange={setIsApproveOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Check className="border-[2px] border-black dark:border-white rounded-md hover:bg-primary cursor-pointer" />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Approve this Business</DialogTitle>
|
||||||
|
<DialogDescription>Are you sure that you will approve this business?</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" variant="default" onClick={onApproveBusiness} disabled={isApproveLoading}>
|
||||||
|
{isApproveLoading ? "Approving..." : "Approve"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isRejectOpen} onOpenChange={setIsRejectOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<X className="border-[2px] border-black dark:border-white rounded-md hover:bg-destructive cursor-pointer" />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Reject this Business</DialogTitle>
|
||||||
|
<DialogDescription>Are you sure that you will reject this business?</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" variant="destructive" onClick={onRejectBusiness} disabled={isRejectLoading}>
|
||||||
|
{isRejectLoading ? "Rejecting..." : "Reject"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/app/admin/business/[businessId]/projects/ProjectAction.tsx
Normal file
104
src/app/admin/business/[businessId]/projects/ProjectAction.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Check, X } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { approveProject, rejectBusiness } from "@/lib/data/applicationMutate";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface ProjectActionsProps {
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectActions({ projectId }: ProjectActionsProps) {
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectOpen, setIsRejectOpen] = useState(false);
|
||||||
|
const [isApproveOpen, setIsApproveOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRejectProject = async () => {
|
||||||
|
try {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
|
||||||
|
const { error } = await rejectBusiness(client, projectId);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast.success("Project rejected successfully");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to reject project");
|
||||||
|
console.error("Failed to reject project:", error);
|
||||||
|
} finally {
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
setIsRejectOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onApproveProject = async () => {
|
||||||
|
try {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
|
||||||
|
const { error } = await approveProject(client, projectId);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast.success("Project approved successfully");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to approve project");
|
||||||
|
console.error("Failed to approve project:", error);
|
||||||
|
} finally {
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
setIsApproveOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Dialog open={isApproveOpen} onOpenChange={setIsApproveOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Check className="border-[2px] border-black dark:border-white rounded-md hover:bg-primary cursor-pointer" />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Approve this Project</DialogTitle>
|
||||||
|
<DialogDescription>Are you sure you want to approve this project?</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" variant="default" onClick={onApproveProject} disabled={isApproveLoading}>
|
||||||
|
{isApproveLoading ? "Approving..." : "Approve"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isRejectOpen} onOpenChange={setIsRejectOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<X className="border-[2px] border-black dark:border-white rounded-md hover:bg-destructive cursor-pointer" />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Reject this Project</DialogTitle>
|
||||||
|
<DialogDescription>Are you sure you want to reject this project?</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" variant="destructive" onClick={onRejectProject} disabled={isRejectLoading}>
|
||||||
|
{isRejectLoading ? "Rejecting..." : "Reject"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
src/app/admin/business/[businessId]/projects/page.tsx
Normal file
139
src/app/admin/business/[businessId]/projects/page.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
|
import { getAllProjectApplicationByBusinessQuery } from "@/lib/data/applicationQuery";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import ProjectActions from "./ProjectAction";
|
||||||
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
|
||||||
|
interface ProjectApplicationData {
|
||||||
|
id: number;
|
||||||
|
created_at: string;
|
||||||
|
deadline: string;
|
||||||
|
status: string;
|
||||||
|
project_name: string;
|
||||||
|
business_id: number;
|
||||||
|
business_name: string;
|
||||||
|
user_id: number;
|
||||||
|
project_type_id: number;
|
||||||
|
project_type_value: string;
|
||||||
|
short_description: string;
|
||||||
|
pitch_deck_url: string | null;
|
||||||
|
project_logo: string | null;
|
||||||
|
min_investment: number;
|
||||||
|
target_investment: number;
|
||||||
|
project_photos: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectApplicationTableProps {
|
||||||
|
projects: ProjectApplicationData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectApplicationTable({ projects }: ProjectApplicationTableProps) {
|
||||||
|
if (!projects || projects.length === 0) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={11} className="text-center h-24 text-muted-foreground">
|
||||||
|
No project applications found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<TableRow key={project.id}>
|
||||||
|
<TableCell>{project.project_name}</TableCell>
|
||||||
|
<TableCell>{project.project_type_value}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{project.project_logo && (
|
||||||
|
<Image
|
||||||
|
src={project.project_logo}
|
||||||
|
alt={`${project.project_name} logo`}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{project.short_description}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{project.pitch_deck_url && (
|
||||||
|
<Link href={project.pitch_deck_url} className="text-blue-500 hover:text-blue-600">
|
||||||
|
Pitch Deck
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{project.min_investment}</TableCell>
|
||||||
|
<TableCell>{project.target_investment}</TableCell>
|
||||||
|
<TableCell>{new Date(project.created_at).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>{new Date(project.deadline).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{project.project_photos ? (
|
||||||
|
project.project_photos.length > 0 && (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{project.project_photos.map((photoUrl, index) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
src={photoUrl}
|
||||||
|
alt={`Project photo ${index + 1}`}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p>No images available</p>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ProjectActions projectId={project.id} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProjectAdminPage({ params }: { params: { businessId: string } }) {
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const { data: projectApplicationData, error: projectApplicationError } =
|
||||||
|
await getAllProjectApplicationByBusinessQuery(client, Number(params.businessId));
|
||||||
|
|
||||||
|
if (projectApplicationError) {
|
||||||
|
console.log(projectApplicationError);
|
||||||
|
return <div>Error loading project applications</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl my-4">
|
||||||
|
<h1 className="text-2xl font-semibold mb-4">
|
||||||
|
{projectApplicationData?.[0]?.business_name || "Business Projects"}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Table className="border-2 border-border rounded-md">
|
||||||
|
<TableCaption>Project Applications for Business</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Project Name</TableHead>
|
||||||
|
<TableHead>Project Type</TableHead>
|
||||||
|
<TableHead>Logo</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead>Pitch Deck</TableHead>
|
||||||
|
<TableHead>Min Investment</TableHead>
|
||||||
|
<TableHead>Target Investment</TableHead>
|
||||||
|
<TableHead>Created At</TableHead>
|
||||||
|
<TableHead>Deadline</TableHead>
|
||||||
|
<TableHead>Photos</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<ProjectApplicationTable projects={projectApplicationData || []} />
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
156
src/app/admin/business/page.tsx
Normal file
156
src/app/admin/business/page.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { getUserRole } from "@/lib/data/userQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FolderOpenDot } from "lucide-react";
|
||||||
|
import { getAllBusinessApplicationQuery } from "@/lib/data/applicationQuery";
|
||||||
|
import { BusinessActionButtons } from "./BusinessActionButtons";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
interface ApplicationData {
|
||||||
|
id: any;
|
||||||
|
user_id: any;
|
||||||
|
username: any;
|
||||||
|
business_type_id: any;
|
||||||
|
business_type_value: any;
|
||||||
|
project_application_id: any;
|
||||||
|
business_name: any;
|
||||||
|
created_at: any;
|
||||||
|
is_in_us: any;
|
||||||
|
is_for_sale: any;
|
||||||
|
pitch_deck_url: any;
|
||||||
|
community_size: any;
|
||||||
|
is_generating_revenue: any;
|
||||||
|
money_raised_to_date: any;
|
||||||
|
location: any;
|
||||||
|
status: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplicationTable({ applications }: { applications: ApplicationData[] }) {
|
||||||
|
if (!applications || applications.length === 0) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={11} className="text-center h-24 text-muted-foreground">
|
||||||
|
No business applications found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return applications.map((application: ApplicationData) => (
|
||||||
|
<TableRow key={application.id}>
|
||||||
|
<TableCell>{application.business_name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link href={`/profile/${application.user_id}`} className="text-blue-500 hover:text-blue-600">
|
||||||
|
{application.username}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{application.pitch_deck_url && (
|
||||||
|
<Link href={application.pitch_deck_url} className="text-blue-500 hover:text-blue-600">
|
||||||
|
{application.pitch_deck_url}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox checked={application.is_in_us} disabled />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox checked={application.is_for_sale} disabled />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox checked={application.is_generating_revenue} disabled />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{application.community_size}</TableCell>
|
||||||
|
<TableCell>{application.money_raised_to_date}</TableCell>
|
||||||
|
<TableCell>{application.location}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{application.project_application_id && (
|
||||||
|
<Link href={`/admin/project/${application.project_application_id}`}>
|
||||||
|
<FolderOpenDot className="border-[2px] border-black dark:border-white rounded-md hover:bg-gray-400 w-full cursor-pointer" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{application.status === "pending" && <BusinessActionButtons businessApplicationId={application.id} />}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BusinesssApplicationAdminPage() {
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const { data: userData, error: userDataError } = await client.auth.getUser();
|
||||||
|
|
||||||
|
if (userDataError) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
const uid = userData.user!.id;
|
||||||
|
const { data: roleData, error: roleDataError } = await getUserRole(client, uid);
|
||||||
|
|
||||||
|
if (roleDataError || roleData!.role != "admin") {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: businessApplicationData, error: businessApplicationError } =
|
||||||
|
await getAllBusinessApplicationQuery(client);
|
||||||
|
|
||||||
|
if (businessApplicationError) {
|
||||||
|
console.log(businessApplicationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingApplications = businessApplicationData?.filter((app) => app.status === "pending") || [];
|
||||||
|
const approvedApplications = businessApplicationData?.filter((app) => app.status === "approve") || [];
|
||||||
|
const rejectedApplications = businessApplicationData?.filter((app) => app.status === "rejecte") || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl my-4">
|
||||||
|
<div className="font-bold text-2xl">Admin Page</div>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<Tabs defaultValue="pending" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="pending">Pending ({pendingApplications.length})</TabsTrigger>
|
||||||
|
<TabsTrigger value="approved">Approved ({approvedApplications.length})</TabsTrigger>
|
||||||
|
<TabsTrigger value="rejected">Rejected ({rejectedApplications.length})</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{["pending", "approved", "rejected"].map((status) => (
|
||||||
|
<TabsContent key={status} value={status}>
|
||||||
|
<Table className="border-2 border-border rounded-md">
|
||||||
|
<TableCaption>{status.charAt(0).toUpperCase() + status.slice(1)} business applications</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Business Name</TableHead>
|
||||||
|
<TableHead>User Account</TableHead>
|
||||||
|
<TableHead>Pitch Deck URL</TableHead>
|
||||||
|
<TableHead>Is In US?</TableHead>
|
||||||
|
<TableHead>Is For Sale?</TableHead>
|
||||||
|
<TableHead>Generate Revenue</TableHead>
|
||||||
|
<TableHead>Community Size</TableHead>
|
||||||
|
<TableHead>Money raised to date</TableHead>
|
||||||
|
<TableHead>Location</TableHead>
|
||||||
|
<TableHead>Project</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<ApplicationTable
|
||||||
|
applications={
|
||||||
|
status === "pending"
|
||||||
|
? pendingApplications
|
||||||
|
: status === "approved"
|
||||||
|
? approvedApplications
|
||||||
|
: rejectedApplications
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/app/admin/page.tsx
Normal file
21
src/app/admin/page.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { getAllBusinesses } from "@/lib/data/businessQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
|
import BusinessTable from "./BusinessTable";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
export default async function AdminPage() {
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const { data, error } = await getAllBusinesses(client);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error fetching businesses: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl my-4">
|
||||||
|
<h1 className="text-2xl font-bold">Business List</h1>
|
||||||
|
<Separator className="my-3" />
|
||||||
|
<BusinessTable businesses={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { getCurrentUserID } from "./userApi";
|
|
||||||
|
|
||||||
export type Deal = {
|
export type Deal = {
|
||||||
deal_amount: number;
|
deal_amount: number;
|
||||||
@ -8,39 +7,134 @@ export type Deal = {
|
|||||||
};
|
};
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
|
|
||||||
export async function getDealList() {
|
export async function getDealList(userId: string | undefined) {
|
||||||
const { data: dealData, error } = await supabase
|
if (!userId) {
|
||||||
|
// console.error("No deal list of this user was found");
|
||||||
|
return []; // Exit on error
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
// get id of investors who invest in the business
|
||||||
|
const { data: dealData, error: dealError } = await supabase
|
||||||
.from("business")
|
.from("business")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,
|
|
||||||
project (
|
project (
|
||||||
id,
|
|
||||||
investment_deal (
|
investment_deal (
|
||||||
deal_amount,
|
|
||||||
created_time,
|
|
||||||
investor_id
|
investor_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.eq("user_id", await getCurrentUserID())
|
.eq("user_id", userId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !dealData) {
|
if (dealError) {
|
||||||
alert(JSON.stringify(error));
|
// alert(JSON.stringify(dealError));
|
||||||
console.error("Error fetching deal list:", error);
|
console.error("Error fetching deal list:", dealError);
|
||||||
} else {
|
return []; // Exit on error
|
||||||
const dealList = dealData.project[0].investment_deal;
|
|
||||||
|
|
||||||
if (!dealList.length) {
|
|
||||||
alert("No data available");
|
|
||||||
return; // Exit early if there's no data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the dealList by created_time in descending order
|
|
||||||
const byCreatedTimeDesc = (a: Deal, b: Deal) =>
|
|
||||||
new Date(b.created_time).getTime() - new Date(a.created_time).getTime();
|
|
||||||
return dealList.sort(byCreatedTimeDesc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dealData || !dealData.project.length) {
|
||||||
|
alert("No project available");
|
||||||
|
return []; // Exit if there's no data
|
||||||
|
}
|
||||||
|
|
||||||
|
const investorIdList = dealData.project[0].investment_deal.map((deal) => deal.investor_id);
|
||||||
|
|
||||||
|
// get investment_deal data then sort by created_time
|
||||||
|
const { data: sortedDealData, error: sortedDealDataError } = await supabase
|
||||||
|
.from("investment_deal")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
deal_amount,
|
||||||
|
created_time,
|
||||||
|
investor_id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.in("investor_id", investorIdList)
|
||||||
|
.order("created_time", { ascending: false });
|
||||||
|
|
||||||
|
if (sortedDealDataError) {
|
||||||
|
alert(JSON.stringify(sortedDealDataError));
|
||||||
|
console.error("Error sorting deal list:", sortedDealDataError);
|
||||||
|
return []; // Exit on error
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(sortedDealData)
|
||||||
|
return sortedDealData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #TODO fix query to be non unique
|
||||||
|
export async function getRecentDealData(userId: string | undefined) {
|
||||||
|
if (!userId) {
|
||||||
|
console.error("User not found");
|
||||||
|
return; // Exit on error
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
const dealList = await getDealList(userId);
|
||||||
|
|
||||||
|
if (!dealList) {
|
||||||
|
// #TODO div error
|
||||||
|
console.error("No deal available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get 5 most recent investor
|
||||||
|
const recentDealList = dealList.slice(0, 5);
|
||||||
|
const recentInvestorIdList = recentDealList.map((deal) => deal.investor_id);
|
||||||
|
|
||||||
|
const { data: recentUserData, error: recentUserError } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
username,
|
||||||
|
avatar_url
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.in("id", recentInvestorIdList);
|
||||||
|
|
||||||
|
if (!recentUserData) {
|
||||||
|
alert("No recent users available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentUserError) {
|
||||||
|
// Handle the error and return a meaningful message
|
||||||
|
console.error("Error fetching profiles:", recentUserError);
|
||||||
|
return;
|
||||||
|
// #TODO div error
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine two arrays
|
||||||
|
const recentDealData = recentDealList.map((item, index) => {
|
||||||
|
return { ...item, ...recentUserData[index] };
|
||||||
|
});
|
||||||
|
|
||||||
|
return recentDealData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #TODO refactor using supabase query instead of manual
|
||||||
|
// #TODO move to util
|
||||||
|
export function convertToGraphData(deals: Deal[]): Record<string, number> {
|
||||||
|
// group by year & month
|
||||||
|
let graphData = deals.reduce(
|
||||||
|
(acc, deal) => {
|
||||||
|
const monthYear = new Date(deal.created_time).toISOString().slice(0, 7); // E.g., '2024-10'
|
||||||
|
acc[monthYear] = (acc[monthYear] || 0) + deal.deal_amount; // Sum the deal_amount
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
); // Change type to Record<string, number>
|
||||||
|
|
||||||
|
// Sort keys in ascending order
|
||||||
|
const sortedKeys = Object.keys(graphData).sort((a, b) => (a > b ? 1 : -1));
|
||||||
|
|
||||||
|
// Create a sorted graph data object
|
||||||
|
const sortedGraphData: Record<string, number> = {};
|
||||||
|
sortedKeys.forEach((key) => {
|
||||||
|
sortedGraphData[key] = graphData[key];
|
||||||
|
});
|
||||||
|
return sortedGraphData;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
import { LoginButton } from "@/components/auth/loginButton";
|
import { LoginButton } from "@/components/auth/loginButton";
|
||||||
@ -9,7 +7,8 @@ export default function Login() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
||||||
style={{ backgroundImage: "url(/login.png)" }}>
|
style={{ backgroundImage: "url(/login.png)" }}
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="items-center">
|
<CardHeader className="items-center">
|
||||||
<CardTitle className="text-2xl font-bold">Empower Your Vision</CardTitle>
|
<CardTitle className="text-2xl font-bold">Empower Your Vision</CardTitle>
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
import { SignupButton } from "@/components/auth/signupButton";
|
import { SignupButton } from "@/components/auth/signupButton";
|
||||||
@ -9,7 +7,8 @@ export default function Signup() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
||||||
style={{ backgroundImage: "url(/signup.png)" }}>
|
style={{ backgroundImage: "url(/signup.png)" }}
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="items-center">
|
<CardHeader className="items-center">
|
||||||
<CardTitle className="text-2xl font-bold">Join Our Community</CardTitle>
|
<CardTitle className="text-2xl font-bold">Join Our Community</CardTitle>
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function ApplyBusiness() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { error } = await supabase
|
||||||
.from("business_application")
|
.from("business_application")
|
||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
@ -59,20 +59,18 @@ export default function ApplyBusiness() {
|
|||||||
is_for_sale: recvData["isForSale"],
|
is_for_sale: recvData["isForSale"],
|
||||||
is_generating_revenue: recvData["isGenerating"],
|
is_generating_revenue: recvData["isGenerating"],
|
||||||
is_in_us: recvData["isInUS"],
|
is_in_us: recvData["isInUS"],
|
||||||
pitch_deck_url:
|
pitch_deck_url: pitchType === "string" ? recvData["businessPitchDeck"] : "",
|
||||||
pitchType === "string" ? recvData["businessPitchDeck"] : "",
|
|
||||||
money_raised_to_date: recvData["totalRaised"],
|
money_raised_to_date: recvData["totalRaised"],
|
||||||
community_size: recvData["communitySize"],
|
community_size: recvData["communitySize"],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.select();
|
.select();
|
||||||
setSucess(true);
|
setSucess(true);
|
||||||
// console.table(data);
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: error == null ? "success" : "error",
|
icon: error == null ? "success" : "error",
|
||||||
title: error == null ? "success" : "Error: " + error.code,
|
title: error == null ? "success" : "Error: " + error.code,
|
||||||
text:
|
text: error == null ? "Your application has been submitted" : error.message,
|
||||||
error == null ? "Your application has been submitted" : error.message,
|
|
||||||
confirmButtonColor: error == null ? "green" : "red",
|
confirmButtonColor: error == null ? "green" : "red",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
@ -80,10 +78,7 @@ export default function ApplyBusiness() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasUserApplied = async (userID: string) => {
|
const hasUserApplied = async (userID: string) => {
|
||||||
let { data: business, error } = await supabase
|
let { data: business, error } = await supabase.from("business").select("*").eq("user_id", userID);
|
||||||
.from("business")
|
|
||||||
.select("*")
|
|
||||||
.eq("user_id", userID);
|
|
||||||
let { data: businessApplication, error: applicationError } = await supabase
|
let { data: businessApplication, error: applicationError } = await supabase
|
||||||
.from("business_application")
|
.from("business_application")
|
||||||
.select("*")
|
.select("*")
|
||||||
@ -93,34 +88,28 @@ export default function ApplyBusiness() {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
console.error(applicationError);
|
console.error(applicationError);
|
||||||
}
|
}
|
||||||
if (
|
if ((business && business.length != 0) || (businessApplication && businessApplication.length != 0)) {
|
||||||
(business && business.length != 0) ||
|
|
||||||
(businessApplication && businessApplication.length != 0)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const transformChoice = (data: any) => {
|
const transformChoice = (data: any) => {
|
||||||
// convert any yes and no to true or false
|
// convert any yes and no to true or false
|
||||||
const transformedData = Object.entries(data).reduce(
|
const transformedData = Object.entries(data).reduce((acc: Record<any, any>, [key, value]) => {
|
||||||
(acc: Record<any, any>, [key, value]) => {
|
if (typeof value === "string") {
|
||||||
if (typeof value === "string") {
|
const lowerValue = value.toLowerCase();
|
||||||
const lowerValue = value.toLowerCase();
|
if (lowerValue === "yes") {
|
||||||
if (lowerValue === "yes") {
|
acc[key] = true;
|
||||||
acc[key] = true;
|
} else if (lowerValue === "no") {
|
||||||
} else if (lowerValue === "no") {
|
acc[key] = false;
|
||||||
acc[key] = false;
|
|
||||||
} else {
|
|
||||||
acc[key] = value; // keep other string values unchanged
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
acc[key] = value; // keep other types unchanged
|
acc[key] = value; // keep other string values unchanged
|
||||||
}
|
}
|
||||||
return acc;
|
} else {
|
||||||
},
|
acc[key] = value; // keep other types unchanged
|
||||||
{}
|
}
|
||||||
);
|
return acc;
|
||||||
|
}, {});
|
||||||
return transformedData;
|
return transformedData;
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -167,12 +156,10 @@ export default function ApplyBusiness() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="mt-5 justify-self-center">
|
<div className="mt-5 justify-self-center">
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
All information submitted in this application is for internal use
|
All information submitted in this application is for internal use only and is treated with the utmost{" "}
|
||||||
only and is treated with the utmost{" "}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
confidentiality. Companies may apply to raise with B2DVentures more
|
confidentiality. Companies may apply to raise with B2DVentures more than once.
|
||||||
than once.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
23
src/app/calendar/page.tsx
Normal file
23
src/app/calendar/page.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { DateTimePicker } from "@/components/ui/datetime-picker";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const DatetimePickerHourCycle = () => {
|
||||||
|
const [date12, setDate12] = useState<Date | undefined>(undefined);
|
||||||
|
const [date24, setDate24] = useState<Date | undefined>(undefined);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 lg:flex-row">
|
||||||
|
<div className="flex w-72 flex-col gap-2">
|
||||||
|
<Label>12 Hour</Label>
|
||||||
|
<DateTimePicker hourCycle={12} value={date12} onChange={setDate12} />
|
||||||
|
</div>
|
||||||
|
<div className="flex w-72 flex-col gap-2">
|
||||||
|
<Label>24 Hour</Label>
|
||||||
|
<DateTimePicker hourCycle={24} value={date24} onChange={setDate24} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatetimePickerHourCycle;
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Deal, getDealList } from "../api/dealApi";
|
import { Deal, getDealList, convertToGraphData, getRecentDealData } from "../api/dealApi";
|
||||||
|
import { RecentDealData } from "@/components/recent-funds";
|
||||||
|
import { getCurrentUserID } from "../api/userApi";
|
||||||
|
|
||||||
// custom hook for deal list
|
// custom hook for deal list
|
||||||
export function useDealList() {
|
export function useDealList() {
|
||||||
const [dealList, setDealList] = useState<Deal[]>();
|
const [dealList, setDealList] = useState<Deal[]>([]);
|
||||||
|
|
||||||
const fetchDealList = async () => {
|
const fetchDealList = async () => {
|
||||||
setDealList(await getDealList());
|
// set the state to the deal list of current business user
|
||||||
|
setDealList(await getDealList(await getCurrentUserID()));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -14,4 +17,37 @@ export function useDealList() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return dealList;
|
return dealList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGraphData() {
|
||||||
|
const [graphData, setGraphData] = useState({});
|
||||||
|
|
||||||
|
const fetchGraphData = async () => {
|
||||||
|
// fetch the state to the deal list of current business user
|
||||||
|
const dealList = await getDealList(await getCurrentUserID());
|
||||||
|
if (dealList) {
|
||||||
|
setGraphData(convertToGraphData(dealList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGraphData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return graphData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRecentDealData() {
|
||||||
|
const [recentDealData, setRecentDealData] = useState<RecentDealData[]>();
|
||||||
|
|
||||||
|
const fetchRecentDealData = async () => {
|
||||||
|
// set the state to the deal list of current business user
|
||||||
|
setRecentDealData(await getRecentDealData(await getCurrentUserID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRecentDealData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return recentDealData;
|
||||||
}
|
}
|
||||||
@ -1,85 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
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";
|
||||||
import { RecentFunds } from "@/components/recent-funds";
|
import { RecentFunds } from "@/components/recent-funds";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useDealList } from "./hook";
|
import { useDealList, useGraphData, useRecentDealData } from "./hook";
|
||||||
|
import { sumByKey } from "@/lib/utils";
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
name: "Jan",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Feb",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mar",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Apr",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "May",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jun",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jul",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Aug",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sep",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Oct",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Nov",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Dec",
|
|
||||||
value: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [graphType, setGraphType] = useState("line");
|
const [graphType, setGraphType] = useState("line");
|
||||||
|
const graphData = useGraphData();
|
||||||
const dealList = useDealList();
|
const dealList = useDealList();
|
||||||
const totalDealAmount =
|
// #TODO dependency injection refactor + define default value inside function (and not here)
|
||||||
dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
|
const recentDealData = useRecentDealData() || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dealList?.map((deal, index) => (
|
|
||||||
<div key={index} className="deal-item">
|
|
||||||
<p>Deal Amount: {deal.deal_amount}</p>
|
|
||||||
<p>Created Time: {new Date(deal.created_time).toUTCString()}</p>
|
|
||||||
<p>Investor ID: {deal.investor_id}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Image
|
<Image
|
||||||
src="/examples/dashboard-light.png"
|
src="/examples/dashboard-light.png"
|
||||||
@ -99,9 +37,7 @@ export default function Dashboard() {
|
|||||||
<div className="hidden flex-col md:flex">
|
<div className="hidden flex-col md:flex">
|
||||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||||
<div className="flex items-center justify-between space-y-2">
|
<div className="flex items-center justify-between space-y-2">
|
||||||
<h2 className="text-3xl font-bold tracking-tight">
|
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
|
||||||
Business Dashboard
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="overview" className="space-y-4">
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
@ -112,9 +48,7 @@ export default function Dashboard() {
|
|||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">Total Funds Raised</CardTitle>
|
||||||
Total Funds Raised
|
|
||||||
</CardTitle>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -129,7 +63,7 @@ export default function Dashboard() {
|
|||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">${totalDealAmount}</div>
|
<div className="text-2xl font-bold">${sumByKey(dealList, "deal_amount")}</div>
|
||||||
{/* <p className="text-xs text-muted-foreground">
|
{/* <p className="text-xs text-muted-foreground">
|
||||||
+20.1% from last month
|
+20.1% from last month
|
||||||
</p> */}
|
</p> */}
|
||||||
@ -137,9 +71,7 @@ export default function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">Profile Views</CardTitle>
|
||||||
Profile Views
|
|
||||||
</CardTitle>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -163,9 +95,7 @@ export default function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">Total Followers</CardTitle>
|
||||||
Total Followers
|
|
||||||
</CardTitle>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -220,23 +150,14 @@ export default function Dashboard() {
|
|||||||
<CardTitle>Overview</CardTitle>
|
<CardTitle>Overview</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-2">
|
<CardContent className="pl-2">
|
||||||
<Overview graphType={graphType} data={data} />
|
<Overview graphType={graphType} graphData={graphData} />
|
||||||
{/* tab to switch between line and bar graph */}
|
{/* tab to switch between line and bar graph */}
|
||||||
<Tabs
|
<Tabs defaultValue="line" className="space-y-4 ml-[50%] mt-2">
|
||||||
defaultValue="line"
|
|
||||||
className="space-y-4 ml-[50%] mt-2"
|
|
||||||
>
|
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger
|
<TabsTrigger value="line" onClick={() => setGraphType("line")}>
|
||||||
value="line"
|
|
||||||
onClick={() => setGraphType("line")}
|
|
||||||
>
|
|
||||||
Line
|
Line
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger value="bar" onClick={() => setGraphType("bar")}>
|
||||||
value="bar"
|
|
||||||
onClick={() => setGraphType("bar")}
|
|
||||||
>
|
|
||||||
Bar
|
Bar
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@ -246,12 +167,10 @@ export default function Dashboard() {
|
|||||||
<Card className="col-span-3">
|
<Card className="col-span-3">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Funds</CardTitle>
|
<CardTitle>Recent Funds</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>You made {dealList?.length || 0} sales this month.</CardDescription>
|
||||||
You made {dealList?.length || 0} sales this month.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<RecentFunds></RecentFunds>
|
<RecentFunds recentDealData={recentDealData}></RecentFunds>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
182
src/app/dataroom/[dataroomId]/files/page.tsx
Normal file
182
src/app/dataroom/[dataroomId]/files/page.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { FileIcon, defaultStyles } from "react-file-icon";
|
||||||
|
import { getFilesByDataroomId } from "@/lib/data/dataroomQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { getAccessRequests } from "@/lib/data/dataroomQuery";
|
||||||
|
import useSession from "@/lib/supabase/useSession";
|
||||||
|
|
||||||
|
export default function ViewDataRoomFilesPage({ params }: { params: { dataroomId: number } }) {
|
||||||
|
const dataroomId = params.dataroomId;
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
const router = useRouter();
|
||||||
|
const [sortOption, setSortOption] = useState("name");
|
||||||
|
const { session, loading: sessionLoading } = useSession();
|
||||||
|
const userId = session?.user?.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionLoading && !session) {
|
||||||
|
toast.error("Please login to access this page");
|
||||||
|
router.push("/auth/login");
|
||||||
|
}
|
||||||
|
}, [session, sessionLoading, router]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: project,
|
||||||
|
error: projectError,
|
||||||
|
isLoading: isLoadingProject,
|
||||||
|
} = useQuery(supabase.from("project").select(`id, project_name, dataroom_id`).eq("dataroom_id", dataroomId), {
|
||||||
|
enabled: !!userId,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: accessRequest,
|
||||||
|
error: accessRequestError,
|
||||||
|
isLoading: accessRequestLoading,
|
||||||
|
} = useQuery(getAccessRequests(supabase, { dataroomId: dataroomId, userId: userId }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessRequestLoading && accessRequest) {
|
||||||
|
const hasAccess = accessRequest[0]?.status === "approve";
|
||||||
|
if (!hasAccess) {
|
||||||
|
toast.error("You don't have permission to access this dataroom");
|
||||||
|
router.push("/dataroom/overview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [accessRequest, accessRequestLoading, router]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: files,
|
||||||
|
error: filesError,
|
||||||
|
isLoading: isLoadingFiles,
|
||||||
|
} = useQuery(getFilesByDataroomId(supabase, dataroomId));
|
||||||
|
|
||||||
|
if (projectError || filesError || accessRequestError) {
|
||||||
|
const errorMessage = projectError
|
||||||
|
? "Unable to load project details"
|
||||||
|
: filesError
|
||||||
|
? "Unable to load files"
|
||||||
|
: "Unable to verify access permissions";
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
router.push("/dataroom/overview");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingProject || isLoadingFiles || accessRequestLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl p-4">
|
||||||
|
<Skeleton className="h-8 w-64 mb-4" />
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<Skeleton key={index} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileNameFromUrl(fileUrl: string): string {
|
||||||
|
const fullFileName = fileUrl.split("/").pop() || "";
|
||||||
|
return decodeURIComponent(fullFileName.split("?")[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedFiles = [...(files || [])].sort((a, b) => {
|
||||||
|
if (sortOption === "name") {
|
||||||
|
const nameA = getFileNameFromUrl(a.file_url).toLowerCase();
|
||||||
|
const nameB = getFileNameFromUrl(b.file_url).toLowerCase();
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
} else if (sortOption === "date") {
|
||||||
|
return new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl p-4">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
|
Dataroom Files |
|
||||||
|
{isLoadingProject ? (
|
||||||
|
"Loading..."
|
||||||
|
) : project && project.length > 0 ? (
|
||||||
|
<Link href={`/deals/${project[0].id}`} className="text-blue-600">
|
||||||
|
{project[0].project_name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
"Project Not Found"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
id="file-section"
|
||||||
|
className="border border-border dark:border-gray-700 rounded-md p-4 space-y-4 bg-white dark:bg-gray-900 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">{`Uploaded files (${sortedFiles.length})`}</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<label htmlFor="sort" className="text-gray-500 dark:text-gray-400">
|
||||||
|
Sort by:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="sort"
|
||||||
|
value={sortOption}
|
||||||
|
onChange={(e) => setSortOption(e.target.value)}
|
||||||
|
className="border border-gray-300 dark:border-gray-600 rounded-md p-1 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conditional rendering based on loading state */}
|
||||||
|
{isLoadingProject || isLoadingFiles ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<Skeleton key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sortedFiles.length > 0 ? (
|
||||||
|
sortedFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.id}
|
||||||
|
className="flex justify-between items-center border border-border dark:border-gray-700 rounded-md p-3 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
|
||||||
|
>
|
||||||
|
<span className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 flex items-center justify-center">
|
||||||
|
<FileIcon
|
||||||
|
extension={file.file_type.value}
|
||||||
|
{...defaultStyles[file.file_type.value as keyof typeof defaultStyles]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link href={file.file_url} rel="noopener noreferrer" target="_blank">
|
||||||
|
<p className="text-blue-600 dark:text-blue-400 hover:text-blue-800">
|
||||||
|
{getFileNameFromUrl(file.file_url)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center space-x-4 text-gray-600 dark:text-gray-400">
|
||||||
|
<p>{"Unknown size"}</p>
|
||||||
|
<p>{format(new Date(file.uploaded_at), "MMM d, yyyy")}</p>
|
||||||
|
<button className="px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">No files uploaded yet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/app/dataroom/manage/AccessRequestsManagement.tsx
Normal file
104
src/app/dataroom/manage/AccessRequestsManagement.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { updateAccessRequestStatus } from "@/lib/data/dataroomMutate";
|
||||||
|
import { getAccessRequests } from "@/lib/data/dataroomQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
|
||||||
|
interface AccessRequest {
|
||||||
|
id: number;
|
||||||
|
dataroom_id: number;
|
||||||
|
user_id: string;
|
||||||
|
status: "pending" | "approve" | "reject";
|
||||||
|
requested_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccessRequestsManagement({ dataroomId }: { dataroomId: number }) {
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useQuery(getAccessRequests(supabase, { dataroomId }));
|
||||||
|
const [accessRequests, setAccessRequests] = useState<AccessRequest[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setAccessRequests(data);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleStatusChange = async (requestId: number, newStatus: "approve" | "reject" | "pending") => {
|
||||||
|
await updateAccessRequestStatus(supabase, requestId, newStatus);
|
||||||
|
setAccessRequests((prevRequests) =>
|
||||||
|
prevRequests.map((request) => (request.id === requestId ? { ...request, status: newStatus } : request))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading access requests...</p>;
|
||||||
|
if (error) return <p>Error loading access requests: {error.message}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Manage Access Requests</h3>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="overflow-y-auto max-h-60">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of access requests for the selected project/dataroom.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>User</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{accessRequests.map((request) => (
|
||||||
|
<TableRow key={request.id}>
|
||||||
|
<TableCell>{request.user_id}</TableCell>
|
||||||
|
<TableCell>{request.status}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{request.status === "pending" ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-green-500 text-white mr-2"
|
||||||
|
onClick={() => handleStatusChange(request.id, "approve")}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-red-500 text-white"
|
||||||
|
onClick={() => handleStatusChange(request.id, "reject")}
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : request.status === "approve" ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-red-500 text-white"
|
||||||
|
onClick={() => handleStatusChange(request.id, "pending")}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-green-500 text-white"
|
||||||
|
onClick={() => handleStatusChange(request.id, "approve")}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
src/app/dataroom/manage/FileManagement.tsx
Normal file
186
src/app/dataroom/manage/FileManagement.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { uploadFileToDataRoom } from "@/lib/data/bucket/uploadFile";
|
||||||
|
import { getFilesByDataroomId } from "@/lib/data/dataroomQuery";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { deleteFileFromDataRoom } from "@/lib/data/bucket/deleteFile";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface FileManagementInterface {
|
||||||
|
dataroomId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Files {
|
||||||
|
id: number;
|
||||||
|
dataroom_id: number;
|
||||||
|
file_url: string;
|
||||||
|
file_type: {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
uploaded_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileManagement({ dataroomId }: FileManagementInterface) {
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
const [fileToDelete, setFileToDelete] = useState<Files | null>(null);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: files, isLoading, error, refetch } = useQuery(getFilesByDataroomId(supabase, dataroomId));
|
||||||
|
|
||||||
|
function getFileNameFromUrl(fileUrl: string): string {
|
||||||
|
const fullFileName = fileUrl.split("/").pop() || "";
|
||||||
|
return decodeURIComponent(fullFileName.split("?")[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteClick = (file: any) => {
|
||||||
|
setFileToDelete(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!fileToDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteFileFromDataRoom(
|
||||||
|
supabase,
|
||||||
|
fileToDelete.dataroom_id,
|
||||||
|
getFileNameFromUrl(fileToDelete.file_url),
|
||||||
|
fileToDelete.id
|
||||||
|
);
|
||||||
|
setFileToDelete(null);
|
||||||
|
refetch();
|
||||||
|
toast.success("Delete successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error occur while deleting file!");
|
||||||
|
setDeleteError("Error occur while deleting file!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.files) {
|
||||||
|
setSelectedFile(event.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadFile = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
setUploadError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadFileToDataRoom(supabase, selectedFile, dataroomId);
|
||||||
|
refetch();
|
||||||
|
toast.success("Upload successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error occur while uploading!");
|
||||||
|
setUploadError("Error occur while uploading!");
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading files...</p>;
|
||||||
|
if (error) return <p>Error loading files: {error.message}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Manage Files</h3>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<Input type="file" className="mb-2" onChange={handleFileChange} />
|
||||||
|
<Button className="mb-4" onClick={handleUploadFile} disabled={uploading}>
|
||||||
|
{uploading ? "Uploading..." : "Upload File"}
|
||||||
|
</Button>
|
||||||
|
{uploadError && <div className="text-red-500">{uploadError}</div>}
|
||||||
|
{deleteError && <div className="text-red-500">{deleteError}</div>}
|
||||||
|
|
||||||
|
<div className="overflow-y-auto max-h-60 mb-4">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of files in the selected data room.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>File Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{files?.map((file) => (
|
||||||
|
<TableRow key={file.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<Link href={file.file_url} rel="noopener noreferrer" target="_blank">
|
||||||
|
<p className="text-blue-600 dark:text-blue-400 hover:text-blue-800">
|
||||||
|
{getFileNameFromUrl(file.file_url)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>Uploaded</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="outline" className="text-red-500" onClick={() => handleDeleteClick(file)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the file "
|
||||||
|
{getFileNameFromUrl(file.file_url)}
|
||||||
|
".
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setFileToDelete(null)}>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteConfirm}>Continue</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>Total Files</TableCell>
|
||||||
|
<TableCell className="text-right">{files?.length}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/app/dataroom/manage/page.tsx
Normal file
110
src/app/dataroom/manage/page.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import FileManagement from "./FileManagement";
|
||||||
|
import AccessRequestsManagement from "./AccessRequestsManagement";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { getProjectByUserId } from "@/lib/data/projectQuery";
|
||||||
|
import { getUserRole } from "@/lib/data/userQuery";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
export default function ManageDataroomPage() {
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [userRole, setUserRole] = useState<string | null>(null);
|
||||||
|
const [projects, setProjects] = useState<any[]>([]);
|
||||||
|
const [selectedProject, setSelectedProject] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserData = async () => {
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
if (!user) {
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: roleData, error: roleError } = await getUserRole(supabase, user.id);
|
||||||
|
if (roleError) {
|
||||||
|
toast.error("Error loading user role.");
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUserRole(roleData.role);
|
||||||
|
|
||||||
|
const { data: projectData, error: projectError } = await getProjectByUserId(supabase, user.id);
|
||||||
|
if (projectError) {
|
||||||
|
toast.error("Error loading projects.");
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProjects(projectData);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserData();
|
||||||
|
}, [supabase, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userRole && userRole !== "business") {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}, [userRole, router]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p className="flex items-center justify-center h-screen">Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-screen-xl p-4">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Manage Data Room / Projects</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const selected = projects.find((project) => project.id.toString() === value);
|
||||||
|
setSelectedProject(selected);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Select a project" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Projects</SelectLabel>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<SelectItem key={project.id} value={project.id.toString()}>
|
||||||
|
{project.project_name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedProject && (
|
||||||
|
<>
|
||||||
|
<div className="border border-border p-4 rounded-md my-3">
|
||||||
|
<FileManagement dataroomId={selectedProject.dataroom_id} />
|
||||||
|
</div>
|
||||||
|
<div className="border border-border p-4 rounded-md">
|
||||||
|
<AccessRequestsManagement dataroomId={selectedProject.dataroom_id} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
216
src/app/dataroom/overview/page.tsx
Normal file
216
src/app/dataroom/overview/page.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { requestAccessToDataRoom } from "@/lib/data/dataroomMutate";
|
||||||
|
import { getInvestmentByUserId } from "@/lib/data/investmentQuery";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableFooter,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getAccessRequests } from "@/lib/data/dataroomQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import useSession from "@/lib/supabase/useSession";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccessRequest {
|
||||||
|
id: number;
|
||||||
|
dataroom_id: number;
|
||||||
|
user_id: string;
|
||||||
|
status: "approve" | "reject" | "pending";
|
||||||
|
requested_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListRequestAccessPage() {
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const { session, loading: sessionLoading } = useSession();
|
||||||
|
const userId = session?.user?.id;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: investments = [],
|
||||||
|
error: investmentsError,
|
||||||
|
isLoading: isLoadingInvestments,
|
||||||
|
} = useQuery(getInvestmentByUserId(client, userId ?? ""), {
|
||||||
|
enabled: !!userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: accessRequests = [],
|
||||||
|
error: accessRequestsError,
|
||||||
|
isLoading: isLoadingAccessRequests,
|
||||||
|
refetch: refetchAccessRequests,
|
||||||
|
} = useQuery(getAccessRequests(client, { userId: userId ?? "" }), {
|
||||||
|
enabled: !!userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const handleRequestAccess = async (dataroomId: any) => {
|
||||||
|
if (!userId) {
|
||||||
|
toast.error("Please login first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await requestAccessToDataRoom(client, dataroomId, userId);
|
||||||
|
if (error) {
|
||||||
|
toast.error("Error sending request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Request sent successfully!");
|
||||||
|
await refetchAccessRequests();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sessionLoading) {
|
||||||
|
return <div className="flex items-center justify-center h-screen">Checking authentication...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return <div className="flex items-center justify-center h-screen">Please login to view this page</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingInvestments || isLoadingAccessRequests) {
|
||||||
|
return <div className="flex items-center justify-center h-screen">Loading data...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (investmentsError || accessRequestsError) {
|
||||||
|
return <div className="flex items-center justify-center h-screen">Error loading data!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-5">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">List of Access Requests</h1>
|
||||||
|
|
||||||
|
<div className="mb-6 space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Project Investments</h2>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
|
{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>
|
||||||
|
{(accessRequests || []).some(
|
||||||
|
(request) => request.dataroom_id === projectInvestments[0].dataroom_id && request.status === "pending"
|
||||||
|
) ? (
|
||||||
|
<Button disabled>Pending Request</Button>
|
||||||
|
) : (accessRequests || []).some(
|
||||||
|
(request) => request.dataroom_id === projectInvestments[0].dataroom_id && request.status === "approve"
|
||||||
|
) ? (
|
||||||
|
<Button onClick={() => window.open(`${projectInvestments[0].dataroom_id}/files`, "_blank")}>
|
||||||
|
Access Dataroom
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => handleRequestAccess(projectInvestments[0].dataroom_id)}>
|
||||||
|
Request Dataroom Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Access Requests Table - Unchanged */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Access Requests & Status</h2>
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of your access requests.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Request ID</TableHead>
|
||||||
|
<TableHead>Dataroom ID</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Requested At</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(accessRequests || []).length > 0 ? (
|
||||||
|
accessRequests?.map((request: AccessRequest) => (
|
||||||
|
<TableRow key={request.id}>
|
||||||
|
<TableCell className="font-medium">{request.id}</TableCell>
|
||||||
|
<TableCell>{request.dataroom_id}</TableCell>
|
||||||
|
<TableCell>{request.status}</TableCell>
|
||||||
|
<TableCell>{new Date(request.requested_at).toLocaleDateString()}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center">
|
||||||
|
No access requests found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/app/error.tsx
Normal file
19
src/app/error.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
interface ErrorProps {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Error({ error, reset }: ErrorProps) {
|
||||||
|
return (
|
||||||
|
<main className="flex justify-center items-center flex-col gap-6">
|
||||||
|
<h1 className="text-3xl font-semibold">Something went wrong!</h1>
|
||||||
|
<p className="text-lg">{error.message}</p>
|
||||||
|
|
||||||
|
<button className="inline-block bg-accent-500 text-primary-800 px-6 py-3 text-lg" onClick={reset}>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,29 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { Suspense } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
||||||
import { ProjectCard } from "@/components/projectCard";
|
import { ProjectCard } from "@/components/projectCard";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { getBusinesses, getInvestmentCounts, getProjects, getTags } from "@/lib/data/query";
|
import { getBusinessAndProject } from "@/lib/data/businessQuery";
|
||||||
import { Tables } from "@/types/database.types";
|
|
||||||
|
|
||||||
interface ProjectInvestmentDetail extends Tables<"ProjectInvestmentDetail"> {}
|
function FindContent() {
|
||||||
|
|
||||||
interface Project extends Tables<"Project"> {
|
|
||||||
ProjectInvestmentDetail: ProjectInvestmentDetail[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Business extends Tables<"Business"> {
|
|
||||||
Projects: Project[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Find() {
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const query = searchParams.get("query");
|
const query = searchParams.get("query");
|
||||||
// const query = "neon";
|
|
||||||
|
|
||||||
let supabase = createSupabaseClient();
|
let supabase = createSupabaseClient();
|
||||||
|
|
||||||
@ -31,95 +19,71 @@ export default function Find() {
|
|||||||
data: businesses,
|
data: businesses,
|
||||||
isLoading: isLoadingBusinesses,
|
isLoading: isLoadingBusinesses,
|
||||||
error: businessError,
|
error: businessError,
|
||||||
} = useQuery(getBusinesses(supabase, query));
|
} = useQuery(getBusinessAndProject(supabase, { businessName: query }));
|
||||||
|
|
||||||
const businessIds = businesses?.map((b) => b.id) || [];
|
const isLoading = isLoadingBusinesses;
|
||||||
const {
|
const error = businessError;
|
||||||
data: projects,
|
|
||||||
isLoading: isLoadingProjects,
|
|
||||||
error: projectError,
|
|
||||||
} = useQuery(getProjects(supabase, businessIds), {
|
|
||||||
enabled: businessIds.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectIds = projects?.map((p) => p.id) || [];
|
|
||||||
const {
|
|
||||||
data: tags,
|
|
||||||
isLoading: isLoadingTags,
|
|
||||||
error: tagError,
|
|
||||||
} = useQuery(getTags(supabase, projectIds), {
|
|
||||||
enabled: projectIds.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: investmentCounts,
|
|
||||||
isLoading: isLoadingInvestments,
|
|
||||||
error: investmentError,
|
|
||||||
} = useQuery(getInvestmentCounts(supabase, projectIds), {
|
|
||||||
enabled: projectIds.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const isLoading = isLoadingBusinesses || isLoadingProjects || isLoadingTags || isLoadingInvestments;
|
|
||||||
const error = businessError || projectError || tagError || investmentError;
|
|
||||||
|
|
||||||
const results: Business[] =
|
|
||||||
businesses?.map((business) => ({
|
|
||||||
...business,
|
|
||||||
Projects:
|
|
||||||
projects
|
|
||||||
?.filter((project) => project.businessId === business.id)
|
|
||||||
.map((project) => ({
|
|
||||||
...project,
|
|
||||||
tags: tags?.filter((tag) => tag.itemId === project.id).map((tag) => tag.Tag.value) || [],
|
|
||||||
investmentCount: investmentCounts?.find((ic) => ic.projectId === project.id)?.count || 0,
|
|
||||||
})) || [],
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
if (isLoading) return <p>Loading...</p>;
|
if (isLoading) return <p>Loading...</p>;
|
||||||
if (error) return <p>Error fetching data: {error.message}</p>;
|
if (error) return <p>Error fetching data: {error.message}</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="container max-w-screen-xl">
|
||||||
<div className="mt-10 mx-[15%]">
|
<div className="mt-4">
|
||||||
<h1 className="text-4xl font-bold">Result</h1>
|
<h1 className="text-4xl font-bold">Result</h1>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|
||||||
{results.length === 0 && <p>No results found.</p>}
|
<Card className="w-full">
|
||||||
{results.length > 0 && (
|
<CardContent className="my-2">
|
||||||
<ul>
|
{businesses!.length === 0 && <p>No results found.</p>}
|
||||||
{results.map((business) => (
|
{businesses!.length > 0 && (
|
||||||
<li key={business.id}>
|
<ul>
|
||||||
<Card className="w-full">
|
{businesses!.map((business) => (
|
||||||
<CardHeader>
|
<li key={business.business_id}>
|
||||||
<CardTitle>{business.businessName}</CardTitle>
|
<Card className="w-full">
|
||||||
<CardDescription>Joined Date: {new Date(business.joinedDate).toLocaleDateString()}</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{business.business_name}</CardTitle>
|
||||||
<CardContent>
|
<CardDescription>
|
||||||
{business.Projects.map((project) => (
|
Joined Date: {new Date(business.joined_date).toLocaleDateString()}
|
||||||
<ProjectCard
|
</CardDescription>
|
||||||
key={project.id}
|
</CardHeader>
|
||||||
name={project.projectName}
|
<CardContent className="grid grid-cols-4 gap-4">
|
||||||
description={project.projectName}
|
{business?.projects && business.projects.length > 0 ? (
|
||||||
joinDate={project.projectName}
|
business.projects.map((project) => (
|
||||||
location={"Bangkok"}
|
<ProjectCard
|
||||||
minInvestment={project.ProjectInvestmentDetail[0]?.minInvestment}
|
key={project.id}
|
||||||
totalInvestor={project.ProjectInvestmentDetail[0]?.totalInvestment}
|
name={project.project_name}
|
||||||
totalRaised={project.ProjectInvestmentDetail[0]?.targetInvestment}
|
description={project.project_short_description}
|
||||||
tags={[]}
|
joinDate={project.published_time}
|
||||||
imageUri={null}
|
location={business.location}
|
||||||
/>
|
minInvestment={project.min_investment}
|
||||||
))}
|
totalInvestor={project.total_investment}
|
||||||
</CardContent>
|
totalRaised={project.target_investment}
|
||||||
</Card>
|
tags={project.tags?.map((tag) => String(tag.tag_value)) || []}
|
||||||
</li>
|
imageUri={project.card_image_url}
|
||||||
))}
|
/>
|
||||||
</ul>
|
))
|
||||||
)}
|
) : (
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<p>No Projects</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Find() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<p>Loading search parameters...</p>}>
|
||||||
|
<FindContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } 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";
|
||||||
|
|||||||
22
src/app/loading.tsx
Normal file
22
src/app/loading.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="container flex items-center justify-center h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-12 w-12 text-gray-600 mx-auto mb-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291l-2.832 2.832A10.003 10.003 0 0112 22v-4a8.001 8.001 0 01-6-5.709z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-lg font-semibold text-gray-600">Loading data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/app/not-found.tsx
Normal file
23
src/app/not-found.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
function NotFound() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center justify-center min-h-screen bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||||
|
<div className="max-w-md text-center p-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">This page could not be found :(</h2>
|
||||||
|
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||||
|
Sorry, the page you are looking for does not exist or has been moved.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-accent-500 text-primary-800 px-8 py-4 text-lg font-medium rounded transition duration-200 hover:bg-accent-600 dark:bg-accent-400 dark:text-primary-800 dark:hover:bg-accent-500"
|
||||||
|
>
|
||||||
|
Go back home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@ -1,237 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import CountUp from "react-countup";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ShareIcon, StarIcon } from "lucide-react";
|
|
||||||
import { Toaster, toast } from "react-hot-toast";
|
|
||||||
import useSession from "@/lib/supabase/useSession";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function Invest() {
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const [tab, setTab] = useState("Pitch");
|
|
||||||
const { session, loading } = useSession();
|
|
||||||
const user = session?.user;
|
|
||||||
const [sessionLoaded, setSessionLoaded] = useState(false);
|
|
||||||
const [isFollow, setIsFollow] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// set sessionLoaded to true once session is confirmed
|
|
||||||
if (!loading) {
|
|
||||||
setSessionLoaded(true);
|
|
||||||
}
|
|
||||||
}, [loading]);
|
|
||||||
const handleClick = (item: string) => {
|
|
||||||
setTab(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShare = () => {
|
|
||||||
const currentUrl = window.location.href;
|
|
||||||
if (document.hasFocus()) {
|
|
||||||
navigator.clipboard.writeText(currentUrl).then(() => {
|
|
||||||
toast.success("URL copied to clipboard!");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleFollow = () => {
|
|
||||||
if (user) {
|
|
||||||
setIsFollow((prevState) => !prevState);
|
|
||||||
// save follow to database
|
|
||||||
} else {
|
|
||||||
redirect("/login");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
// percent success
|
|
||||||
const timer = setTimeout(() => setProgress(66), 500);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Toaster position="top-right" reverseOrder={false} />
|
|
||||||
</div>
|
|
||||||
<div className="w-[90%] h-[450px]-500 md:m-auto mt-12 md:mt-12 pl-14 md:pl-24">
|
|
||||||
<div>
|
|
||||||
{/* Name, star and share button packed */}
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="flex col-span-2">
|
|
||||||
<Image
|
|
||||||
src="./logo.svg"
|
|
||||||
alt="logo"
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
className="sm:scale-75"
|
|
||||||
/>
|
|
||||||
<div className="mt-3 font-bold text-lg md:text-3xl">NVIDIA</div>
|
|
||||||
</div>
|
|
||||||
<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={isFollow ? "#FFFF00" : "#fff"}
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Follow NIVIDIA</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<div onClick={handleShare} className=" cursor-pointer mt-2">
|
|
||||||
<ShareIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* end of pack */}
|
|
||||||
<p className="mt-2 sm:text-sm">
|
|
||||||
{" "}
|
|
||||||
World's first non-metal sustainable battery
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap mt-3">
|
|
||||||
{["Technology", "Gaming"].map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1 mb-1"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 mt-5">
|
|
||||||
{/* image carousel */}
|
|
||||||
<div>
|
|
||||||
<Carousel className="w-full mt-20 md:mt-0">
|
|
||||||
<CarouselContent className="h-[400px] flex h-full">
|
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<CarouselItem key={index}>
|
|
||||||
<img
|
|
||||||
src="./boiler1.jpg"
|
|
||||||
alt=""
|
|
||||||
className="rounded-lg self-center"
|
|
||||||
/>
|
|
||||||
</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>{" "}
|
|
||||||
<CarouselPrevious />
|
|
||||||
<CarouselNext />
|
|
||||||
</Carousel>
|
|
||||||
<Carousel className="w-2/3 md:w-full ml-10 md:ml-0 mt-5 md:mt-10 ">
|
|
||||||
<CarouselContent>
|
|
||||||
{/* boiler plate for an actual pictures */}
|
|
||||||
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
|
|
||||||
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
|
|
||||||
</CarouselItem>
|
|
||||||
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
|
|
||||||
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
|
|
||||||
</CarouselItem>
|
|
||||||
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
|
|
||||||
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
|
|
||||||
</CarouselItem>
|
|
||||||
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
|
|
||||||
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
|
|
||||||
</CarouselItem>
|
|
||||||
<CarouselItem className="pl-1 md:basis-1/2 lg:basis-1/3">
|
|
||||||
<img src="./boiler1.jpg" alt="" className="rounded-lg" />
|
|
||||||
</CarouselItem>
|
|
||||||
</CarouselContent>
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
<div className=" w-2/3 mt-4 m-auto grid-rows-5">
|
|
||||||
<div className="pl-5">
|
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8">
|
|
||||||
<CountUp
|
|
||||||
start={0}
|
|
||||||
end={100000}
|
|
||||||
duration={2}
|
|
||||||
prefix="$"
|
|
||||||
className=""
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm md:text-lg"> 5% raised of $5M max goal</p>
|
|
||||||
<Progress value={progress} className="w-[60%] h-3 mt-3" />
|
|
||||||
<h1 className="font-semibold text-4xl md:mt-8">
|
|
||||||
{" "}
|
|
||||||
<CountUp
|
|
||||||
start={0}
|
|
||||||
end={1000}
|
|
||||||
duration={2}
|
|
||||||
className="text-xl md:text-4xl"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm md:text-lg"> Investors</p>
|
|
||||||
</div>
|
|
||||||
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
|
||||||
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5">
|
|
||||||
<CountUp
|
|
||||||
start={0}
|
|
||||||
end={5800}
|
|
||||||
duration={2}
|
|
||||||
className="text-xl md:text-4xl"
|
|
||||||
/>{" "}
|
|
||||||
hours
|
|
||||||
</h1>
|
|
||||||
<p className="ml-5"> Left to invest</p>
|
|
||||||
<Button className="mt-5 md:mt-10 ml-0 md:ml-[25%] scale-75 md:scale-100">
|
|
||||||
<Link href="/invest">Invest in NVIDIA</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* menu */}
|
|
||||||
<div className="flex w-[90%] mt-24 m-auto ml-10 md:ml-32">
|
|
||||||
<ul className="list-none flex gap-10 text-lg md:text-xl ">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={() => handleClick("Pitch")}
|
|
||||||
className={tab === "Pitch" ? "text-blue-600" : ""}
|
|
||||||
>
|
|
||||||
Pitch
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={() => handleClick("General Data")}
|
|
||||||
className={tab === "General Data" ? "text-blue-600" : ""}
|
|
||||||
>
|
|
||||||
General Data
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={() => handleClick("Updates")}
|
|
||||||
className={tab === "Updates" ? "text-blue-600" : ""}
|
|
||||||
>
|
|
||||||
Updates
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<hr className="mt-2" />
|
|
||||||
{/* Card section */}
|
|
||||||
<div className="flex w-full mt-10">
|
|
||||||
{/* Cards */}
|
|
||||||
<Card className="m-auto border-slate-800 w-3/4 p-6">
|
|
||||||
<CardContent>
|
|
||||||
<Card>
|
|
||||||
<CardContent>{tab}</CardContent>
|
|
||||||
</Card>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,13 +1,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ProjectCard } from "@/components/projectCard";
|
import { ProjectCard } from "@/components/projectCard";
|
||||||
import { getTopProjects } from "@/lib/data/projectQuery";
|
import { getTopProjects } from "@/lib/data/projectQuery";
|
||||||
@ -47,17 +41,12 @@ const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
|
|||||||
imageUri={project.project_logo}
|
imageUri={project.project_logo}
|
||||||
joinDate={new Date(project.published_time).toLocaleDateString()}
|
joinDate={new Date(project.published_time).toLocaleDateString()}
|
||||||
location={project.business[0]?.location || ""}
|
location={project.business[0]?.location || ""}
|
||||||
tags={project.project_tag.flatMap(
|
tags={project.project_tag.flatMap((item: { tag: { id: number; value: string }[] }) =>
|
||||||
(item: { tag: { id: number; value: string }[] }) =>
|
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
|
||||||
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
|
|
||||||
)}
|
)}
|
||||||
minInvestment={
|
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
|
||||||
project.project_investment_detail[0]?.min_investment || 0
|
|
||||||
}
|
|
||||||
totalInvestor={0}
|
totalInvestor={0}
|
||||||
totalRaised={
|
totalRaised={project.project_investment_detail[0]?.total_investment || 0}
|
||||||
project.project_investment_detail[0]?.total_investment || 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@ -68,17 +57,13 @@ const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
|
|||||||
const ProjectsLoader = () => (
|
const ProjectsLoader = () => (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{[...Array(4)].map((_, index) => (
|
{[...Array(4)].map((_, index) => (
|
||||||
<div
|
<div key={index} className="h-64 bg-gray-200 animate-pulse rounded-lg"></div>
|
||||||
key={index}
|
|
||||||
className="h-64 bg-gray-200 animate-pulse rounded-lg"
|
|
||||||
></div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
const { data: topProjectsData, error: topProjectsError } =
|
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
|
||||||
await getTopProjects(supabase);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
@ -87,14 +72,9 @@ export default async function Home() {
|
|||||||
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
|
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
|
||||||
<div className="container max-w-screen-xl flex flex-col">
|
<div className="container max-w-screen-xl flex flex-col">
|
||||||
<span className="mx-20 px-10 py-10">
|
<span className="mx-20 px-10 py-10">
|
||||||
<p className="text-4xl font-bold">
|
<p className="text-4xl font-bold">Explore the world of ventures</p>
|
||||||
Explore the world of ventures
|
|
||||||
</p>
|
|
||||||
<span className="text-lg">
|
<span className="text-lg">
|
||||||
<p>
|
<p>Unlock opportunities and connect with a community of passionate</p>
|
||||||
Unlock opportunities and connect with a community of
|
|
||||||
passionate
|
|
||||||
</p>
|
|
||||||
<p>investors and innovators.</p>
|
<p>investors and innovators.</p>
|
||||||
<p>Together, we turn ideas into impact.</p>
|
<p>Together, we turn ideas into impact.</p>
|
||||||
</span>
|
</span>
|
||||||
@ -142,27 +122,11 @@ export default async function Home() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex gap-2">
|
<CardContent className="flex gap-2">
|
||||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||||
<div className="dark:bg-white rounded-full">
|
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||||
<Image
|
|
||||||
src={"/github.svg"}
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
alt="github"
|
|
||||||
className="scale-75 md:scale-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
Github
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||||
<div className="dark:bg-white rounded-full">
|
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||||
<Image
|
|
||||||
src={"/github.svg"}
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
alt="github"
|
|
||||||
className="scale-75 md:scale-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
Github
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -174,13 +138,15 @@ export default async function Home() {
|
|||||||
<div className="flex flex-col px-10">
|
<div className="flex flex-col px-10">
|
||||||
<span className="pb-5">
|
<span className="pb-5">
|
||||||
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
||||||
<p className="text-md md:text-lg">
|
<p className="text-md md:text-lg">The deals attracting the most interest right now</p>
|
||||||
The deals attracting the most interest right now
|
|
||||||
</p>
|
|
||||||
</span>
|
</span>
|
||||||
<Suspense fallback={<ProjectsLoader />}>
|
{topProjectsError ? (
|
||||||
<TopProjects projects={topProjectsData || []} />
|
<div className="text-red-500">Error fetching projects: {topProjectsError}</div>
|
||||||
</Suspense>
|
) : (
|
||||||
|
<Suspense fallback={<ProjectsLoader />}>
|
||||||
|
<TopProjects projects={topProjectsData || []} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
<div className="self-center py-5 scale-75 md:scale-100">
|
<div className="self-center py-5 scale-75 md:scale-100">
|
||||||
<Button>
|
<Button>
|
||||||
<Link href={"/deals"}>View all</Link>
|
<Link href={"/deals"}>View all</Link>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import ProjectForm from "@/components/ProjectForm";
|
import ProjectForm from "@/components/ProjectForm";
|
||||||
import { projectFormSchema } from "@/types/schemas/application.schema";
|
import { projectFormSchema } from "@/types/schemas/application.schema";
|
||||||
@ -29,8 +30,7 @@ export default function ApplyProject() {
|
|||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
pitch_deck_url:
|
pitch_deck_url: pitchType === "string" ? recvData["projectPitchDeck"] : "",
|
||||||
pitchType === "string" ? recvData["projectPitchDeck"] : "",
|
|
||||||
target_investment: recvData["targetInvest"],
|
target_investment: recvData["targetInvest"],
|
||||||
deadline: recvData["deadline"],
|
deadline: recvData["deadline"],
|
||||||
project_name: recvData["projectName"],
|
project_name: recvData["projectName"],
|
||||||
@ -58,36 +58,21 @@ export default function ApplyProject() {
|
|||||||
const results = await Promise.all(tagPromises);
|
const results = await Promise.all(tagPromises);
|
||||||
|
|
||||||
// Collect errors
|
// Collect errors
|
||||||
const errors = results
|
const errors = results.filter((result) => result.error).map((result) => result.error);
|
||||||
.filter((result) => result.error)
|
|
||||||
.map((result) => result.error);
|
|
||||||
|
|
||||||
return { errors };
|
return { errors };
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadPitchFile = async (
|
const uploadPitchFile = async (file: File, userId: string, projectId: string) => {
|
||||||
file: File,
|
|
||||||
userId: string,
|
|
||||||
projectId: string
|
|
||||||
) => {
|
|
||||||
if (!file || !userId) {
|
if (!file || !userId) {
|
||||||
console.error("Pitch file or user ID is undefined.");
|
console.error("Pitch file or user ID is undefined.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await uploadFile(
|
return await uploadFile(file, BUCKET_PITCH_APPLICATION_NAME, `${userId}/${projectId}/pitches/${file.name}`);
|
||||||
file,
|
|
||||||
BUCKET_PITCH_APPLICATION_NAME,
|
|
||||||
`${userId}/${projectId}/pitches/${file.name}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadLogoAndPhotos = async (
|
const uploadLogoAndPhotos = async (logoFile: File, photos: File[], userId: string, projectId: string) => {
|
||||||
logoFile: File,
|
|
||||||
photos: File[],
|
|
||||||
userId: string,
|
|
||||||
projectId: string
|
|
||||||
) => {
|
|
||||||
const uploadResults: { logo?: any; photos: any[] } = { photos: [] };
|
const uploadResults: { logo?: any; photos: any[] } = { photos: [] };
|
||||||
|
|
||||||
// upload logo
|
// upload logo
|
||||||
@ -108,11 +93,7 @@ export default function ApplyProject() {
|
|||||||
|
|
||||||
// upload each photo
|
// upload each photo
|
||||||
const uploadPhotoPromises = photos.map((image) =>
|
const uploadPhotoPromises = photos.map((image) =>
|
||||||
uploadFile(
|
uploadFile(image, BUCKET_PITCH_APPLICATION_NAME, `${userId}/${projectId}/photos/${image.name}`)
|
||||||
image,
|
|
||||||
BUCKET_PITCH_APPLICATION_NAME,
|
|
||||||
`${userId}/${projectId}/photos/${image.name}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const photoResults = await Promise.all(uploadPhotoPromises);
|
const photoResults = await Promise.all(uploadPhotoPromises);
|
||||||
@ -132,8 +113,7 @@ export default function ApplyProject() {
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: error == null ? "success" : "error",
|
icon: error == null ? "success" : "error",
|
||||||
title: error == null ? "Success" : `Error: ${error.code}`,
|
title: error == null ? "Success" : `Error: ${error.code}`,
|
||||||
text:
|
text: error == null ? "Your application has been submitted" : error.message,
|
||||||
error == null ? "Your application has been submitted" : error.message,
|
|
||||||
confirmButtonColor: error == null ? "green" : "red",
|
confirmButtonColor: error == null ? "green" : "red",
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
@ -168,11 +148,7 @@ export default function ApplyProject() {
|
|||||||
|
|
||||||
// upload pitch file if it’s a file
|
// upload pitch file if it’s a file
|
||||||
if (typeof recvData["projectPitchDeck"] === "object") {
|
if (typeof recvData["projectPitchDeck"] === "object") {
|
||||||
const uploadPitchSuccess = await uploadPitchFile(
|
const uploadPitchSuccess = await uploadPitchFile(recvData["projectPitchDeck"], user.id, projectId);
|
||||||
recvData["projectPitchDeck"],
|
|
||||||
user.id,
|
|
||||||
projectId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!uploadPitchSuccess) {
|
if (!uploadPitchSuccess) {
|
||||||
console.error("Error uploading pitch file.");
|
console.error("Error uploading pitch file.");
|
||||||
@ -196,21 +172,11 @@ export default function ApplyProject() {
|
|||||||
// console.log("Logo Path:", logo.data.path);
|
// console.log("Logo Path:", logo.data.path);
|
||||||
// console.table(photos);
|
// console.table(photos);
|
||||||
|
|
||||||
const logoURL = await getPrivateURL(
|
const logoURL = await getPrivateURL(logo.data.path, BUCKET_PITCH_APPLICATION_NAME);
|
||||||
logo.data.path,
|
|
||||||
BUCKET_PITCH_APPLICATION_NAME
|
|
||||||
);
|
|
||||||
let photoURLsArray: string[] = [];
|
let photoURLsArray: string[] = [];
|
||||||
const photoURLPromises = photos.map(
|
const photoURLPromises = photos.map(
|
||||||
async (item: {
|
async (item: { success: boolean; errors: typeof errors; data: { path: string } }) => {
|
||||||
success: boolean;
|
const photoURL = await getPrivateURL(item.data.path, BUCKET_PITCH_APPLICATION_NAME);
|
||||||
errors: typeof errors;
|
|
||||||
data: { path: string };
|
|
||||||
}) => {
|
|
||||||
const photoURL = await getPrivateURL(
|
|
||||||
item.data.path,
|
|
||||||
BUCKET_PITCH_APPLICATION_NAME
|
|
||||||
);
|
|
||||||
if (photoURL?.signedUrl) {
|
if (photoURL?.signedUrl) {
|
||||||
photoURLsArray.push(photoURL.signedUrl);
|
photoURLsArray.push(photoURL.signedUrl);
|
||||||
} else {
|
} else {
|
||||||
@ -233,11 +199,7 @@ export default function ApplyProject() {
|
|||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
displayAlert(error);
|
displayAlert(error);
|
||||||
};
|
};
|
||||||
const updateImageURL = async (
|
const updateImageURL = async (url: string | string[], columnName: string, projectId: number) => {
|
||||||
url: string | string[],
|
|
||||||
columnName: string,
|
|
||||||
projectId: number
|
|
||||||
) => {
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from("project_application")
|
.from("project_application")
|
||||||
.update({ [columnName]: url })
|
.update({ [columnName]: url })
|
||||||
@ -250,9 +212,7 @@ export default function ApplyProject() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getPrivateURL = async (path: string, bucketName: string) => {
|
const getPrivateURL = async (path: string, bucketName: string) => {
|
||||||
const { data } = await supabase.storage
|
const { data } = await supabase.storage.from(bucketName).createSignedUrl(path, 9999999999999999999999999999);
|
||||||
.from(bucketName)
|
|
||||||
.createSignedUrl(path, 9999999999999999999999999999);
|
|
||||||
// console.table(data);
|
// console.table(data);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@ -265,12 +225,10 @@ export default function ApplyProject() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="mt-5 justify-self-center">
|
<div className="mt-5 justify-self-center">
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
Begin Your First Fundraising Project. Starting a fundraising project
|
Begin Your First Fundraising Project. Starting a fundraising project is mandatory for all businesses.
|
||||||
is mandatory for all businesses.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
This step is crucial to begin your journey and unlock the necessary
|
This step is crucial to begin your journey and unlock the necessary tools for raising funds.
|
||||||
tools for raising funds.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,15 +3,7 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DualOptionSelector } from "@/components/dualSelector";
|
import { DualOptionSelector } from "@/components/dualSelector";
|
||||||
import { MultipleOptionSelector } from "@/components/multipleSelector";
|
import { MultipleOptionSelector } from "@/components/multipleSelector";
|
||||||
import {
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { businessFormSchema } from "@/types/schemas/application.schema";
|
import { businessFormSchema } from "@/types/schemas/application.schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -24,9 +16,7 @@ type businessSchema = z.infer<typeof businessFormSchema>;
|
|||||||
interface BusinessFormProps {
|
interface BusinessFormProps {
|
||||||
onSubmit: SubmitHandler<businessSchema>;
|
onSubmit: SubmitHandler<businessSchema>;
|
||||||
}
|
}
|
||||||
const BusinessForm = ({
|
const BusinessForm = ({ onSubmit }: BusinessFormProps & { onSubmit: SubmitHandler<businessSchema> }) => {
|
||||||
onSubmit,
|
|
||||||
}: BusinessFormProps & { onSubmit: SubmitHandler<businessSchema> }) => {
|
|
||||||
const communitySize = [
|
const communitySize = [
|
||||||
{ id: 1, name: "N/A" },
|
{ id: 1, name: "N/A" },
|
||||||
{ id: 2, name: "0-5K" },
|
{ id: 2, name: "0-5K" },
|
||||||
@ -43,14 +33,10 @@ const BusinessForm = ({
|
|||||||
let supabase = createSupabaseClient();
|
let supabase = createSupabaseClient();
|
||||||
const [businessPitch, setBusinessPitch] = useState("text");
|
const [businessPitch, setBusinessPitch] = useState("text");
|
||||||
const [businessPitchFile, setBusinessPitchFile] = useState("");
|
const [businessPitchFile, setBusinessPitchFile] = useState("");
|
||||||
const [countries, setCountries] = useState<{ id: number; name: string }[]>(
|
const [countries, setCountries] = useState<{ id: number; name: string }[]>([]);
|
||||||
[]
|
|
||||||
);
|
|
||||||
const [industry, setIndustry] = useState<{ id: number; name: string }[]>([]);
|
const [industry, setIndustry] = useState<{ id: number; name: string }[]>([]);
|
||||||
const fetchIndustry = async () => {
|
const fetchIndustry = async () => {
|
||||||
let { data: BusinessType, error } = await supabase
|
let { data: BusinessType, error } = await supabase.from("business_type").select("id, value");
|
||||||
.from("business_type")
|
|
||||||
.select("id, value");
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -73,18 +59,12 @@ const BusinessForm = ({
|
|||||||
throw new Error("Network response was not ok");
|
throw new Error("Network response was not ok");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const countryList = data.map(
|
const countryList = data.map((country: { name: { common: string } }, index: number) => ({
|
||||||
(country: { name: { common: string } }, index: number) => ({
|
id: index + 1,
|
||||||
id: index + 1,
|
name: country.name.common,
|
||||||
name: country.name.common,
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setCountries(
|
setCountries(countryList.sort((a: { name: string }, b: { name: any }) => a.name.localeCompare(b.name)));
|
||||||
countryList.sort((a: { name: string }, b: { name: any }) =>
|
|
||||||
a.name.localeCompare(b.name)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching countries:", error);
|
console.error("Error fetching countries:", error);
|
||||||
}
|
}
|
||||||
@ -95,15 +75,11 @@ const BusinessForm = ({
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form onSubmit={form.handleSubmit(onSubmit as SubmitHandler<businessSchema>)} className="space-y-8">
|
||||||
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<businessSchema>)}
|
|
||||||
className="space-y-8"
|
|
||||||
>
|
|
||||||
<div className="grid grid-flow-row auto-rows-max w-3/4 ml-1/2 md:ml-[0%] ">
|
<div className="grid grid-flow-row auto-rows-max w-3/4 ml-1/2 md:ml-[0%] ">
|
||||||
<h1 className="text-3xl font-bold mt-10 ml-96">About your company</h1>
|
<h1 className="text-3xl font-bold mt-10 ml-96">About your company</h1>
|
||||||
<p className="ml-96 mt-5 text-neutral-500">
|
<p className="ml-96 mt-5 text-neutral-500">
|
||||||
<span className="text-red-500 font-bold">**</span>All requested
|
<span className="text-red-500 font-bold">**</span>All requested information in this section is required.
|
||||||
information in this section is required.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="ml-96 mt-5 space-y-10">
|
<div className="ml-96 mt-5 space-y-10">
|
||||||
{/* Company Name */}
|
{/* Company Name */}
|
||||||
@ -112,21 +88,13 @@ const BusinessForm = ({
|
|||||||
name="companyName"
|
name="companyName"
|
||||||
render={({ field }: { field: any }) => (
|
render={({ field }: { field: any }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="font-bold text-lg">
|
<FormLabel className="font-bold text-lg">Company name</FormLabel>
|
||||||
Company name
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="mt-10 space-y-5">
|
<div className="mt-10 space-y-5">
|
||||||
<div className="flex space-x-5">
|
<div className="flex space-x-5">
|
||||||
<Input
|
<Input type="text" id="companyName" className="w-96" {...field} />
|
||||||
type="text"
|
|
||||||
id="companyName"
|
|
||||||
className="w-96"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<span className="text-[12px] text-neutral-500 self-center">
|
<span className="text-[12px] text-neutral-500 self-center">
|
||||||
This should be the name your company uses on your{" "}
|
This should be the name your company uses on your <br />
|
||||||
<br />
|
|
||||||
website and in the market.
|
website and in the market.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -151,9 +119,7 @@ const BusinessForm = ({
|
|||||||
// console.log("Country selected: " + selectedValues.name);
|
// console.log("Country selected: " + selectedValues.name);
|
||||||
field.onChange(selectedValues.name);
|
field.onChange(selectedValues.name);
|
||||||
}}
|
}}
|
||||||
description={
|
description={<>Select the country where your business is based.</>}
|
||||||
<>Select the country where your business is based.</>
|
|
||||||
}
|
|
||||||
placeholder="Select a country"
|
placeholder="Select a country"
|
||||||
selectLabel="Country"
|
selectLabel="Country"
|
||||||
/>
|
/>
|
||||||
@ -178,12 +144,7 @@ const BusinessForm = ({
|
|||||||
// console.log("Type of selected value:", selectedValues.id);
|
// console.log("Type of selected value:", selectedValues.id);
|
||||||
field.onChange(selectedValues.id);
|
field.onChange(selectedValues.id);
|
||||||
}}
|
}}
|
||||||
description={
|
description={<>Choose the industry that best aligns with your business.</>}
|
||||||
<>
|
|
||||||
Choose the industry that best aligns with your
|
|
||||||
business.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placeholder="Select an industry"
|
placeholder="Select an industry"
|
||||||
selectLabel="Industry"
|
selectLabel="Industry"
|
||||||
/>
|
/>
|
||||||
@ -218,8 +179,7 @@ const BusinessForm = ({
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
<span className="text-[12px] text-neutral-500 self-center">
|
<span className="text-[12px] text-neutral-500 self-center">
|
||||||
The sum total of past financing, including angel or
|
The sum total of past financing, including angel or venture <br />
|
||||||
venture <br />
|
|
||||||
capital, loans, grants, or token sales.
|
capital, loans, grants, or token sales.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -240,11 +200,7 @@ const BusinessForm = ({
|
|||||||
<div className="flex space-x-5">
|
<div className="flex space-x-5">
|
||||||
<DualOptionSelector
|
<DualOptionSelector
|
||||||
name="isInUS"
|
name="isInUS"
|
||||||
label={
|
label={<>Is your company incorporated in the United States?</>}
|
||||||
<>
|
|
||||||
Is your company incorporated in the United States?
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
choice1="Yes"
|
choice1="Yes"
|
||||||
choice2="No"
|
choice2="No"
|
||||||
handleFunction={(selectedValues: string) => {
|
handleFunction={(selectedValues: string) => {
|
||||||
@ -255,8 +211,7 @@ const BusinessForm = ({
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
<span className="text-[12px] text-neutral-500 self-center">
|
<span className="text-[12px] text-neutral-500 self-center">
|
||||||
Only companies that are incorporated or formed in the US
|
Only companies that are incorporated or formed in the US are eligible to raise via Reg CF.
|
||||||
are eligible to raise via Reg CF.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -276,21 +231,14 @@ const BusinessForm = ({
|
|||||||
<DualOptionSelector
|
<DualOptionSelector
|
||||||
name="isForSale"
|
name="isForSale"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
label={
|
label={<>Is your product available (for sale) in market?</>}
|
||||||
<>Is your product available (for sale) in market?</>
|
|
||||||
}
|
|
||||||
choice1="Yes"
|
choice1="Yes"
|
||||||
choice2="No"
|
choice2="No"
|
||||||
handleFunction={(selectedValues: string) => {
|
handleFunction={(selectedValues: string) => {
|
||||||
// setIsForSale;
|
// setIsForSale;
|
||||||
field.onChange(selectedValues);
|
field.onChange(selectedValues);
|
||||||
}}
|
}}
|
||||||
description={
|
description={<>Only check this box if customers can access, use, or buy your product today.</>}
|
||||||
<>
|
|
||||||
Only check this box if customers can access, use, or
|
|
||||||
buy your product today.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -317,10 +265,7 @@ const BusinessForm = ({
|
|||||||
field.onChange(selectedValues);
|
field.onChange(selectedValues);
|
||||||
}}
|
}}
|
||||||
description={
|
description={
|
||||||
<>
|
<>Only check this box if your company is making money. Please elaborate on revenue below.</>
|
||||||
Only check this box if your company is making money.
|
|
||||||
Please elaborate on revenue below.
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -345,9 +290,7 @@ const BusinessForm = ({
|
|||||||
<div className="flex space-x-2 w-96">
|
<div className="flex space-x-2 w-96">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={
|
variant={businessPitch === "text" ? "default" : "outline"}
|
||||||
businessPitch === "text" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
onClick={() => setBusinessPitch("text")}
|
onClick={() => setBusinessPitch("text")}
|
||||||
className="w-32 h-12 text-base"
|
className="w-32 h-12 text-base"
|
||||||
>
|
>
|
||||||
@ -355,9 +298,7 @@ const BusinessForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={
|
variant={businessPitch === "file" ? "default" : "outline"}
|
||||||
businessPitch === "file" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
onClick={() => setBusinessPitch("file")}
|
onClick={() => setBusinessPitch("file")}
|
||||||
className="w-32 h-12 text-base"
|
className="w-32 h-12 text-base"
|
||||||
>
|
>
|
||||||
@ -367,14 +308,8 @@ const BusinessForm = ({
|
|||||||
<div className="flex space-x-5">
|
<div className="flex space-x-5">
|
||||||
<Input
|
<Input
|
||||||
type={businessPitch === "file" ? "file" : "text"}
|
type={businessPitch === "file" ? "file" : "text"}
|
||||||
placeholder={
|
placeholder={businessPitch === "file" ? "Upload your Markdown file" : "https:// "}
|
||||||
businessPitch === "file"
|
accept={businessPitch === "file" ? ".md" : undefined}
|
||||||
? "Upload your Markdown file"
|
|
||||||
: "https:// "
|
|
||||||
}
|
|
||||||
accept={
|
|
||||||
businessPitch === "file" ? ".md" : undefined
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target;
|
const value = e.target;
|
||||||
if (businessPitch === "file") {
|
if (businessPitch === "file") {
|
||||||
@ -388,17 +323,12 @@ const BusinessForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-[12px] text-neutral-500 self-center">
|
<span className="text-[12px] text-neutral-500 self-center">
|
||||||
Your pitch deck and other application info will be
|
Your pitch deck and other application info will be used for <br />
|
||||||
used for <br />
|
|
||||||
internal purposes only. <br />
|
internal purposes only. <br />
|
||||||
Please make sure this document is publicly
|
Please make sure this document is publicly accessible. This can <br />
|
||||||
accessible. This can <br />
|
be a DocSend, Box, Dropbox, Google Drive or other link.
|
||||||
be a DocSend, Box, Dropbox, Google Drive or other
|
|
||||||
link.
|
|
||||||
<br />
|
<br />
|
||||||
<p className="text-red-500">
|
<p className="text-red-500">** support only markdown(.md) format</p>
|
||||||
** support only markdown(.md) format
|
|
||||||
</p>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{businessPitchFile && (
|
{businessPitchFile && (
|
||||||
@ -437,10 +367,7 @@ const BusinessForm = ({
|
|||||||
field.onChange(selectedValues.name);
|
field.onChange(selectedValues.name);
|
||||||
}}
|
}}
|
||||||
description={
|
description={
|
||||||
<>
|
<>Include your email list, social media following (e.g., Instagram, Discord, Twitter).</>
|
||||||
Include your email list, social media following (e.g.,
|
|
||||||
Instagram, Discord, Twitter).
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
placeholder="Select"
|
placeholder="Select"
|
||||||
selectLabel="Select"
|
selectLabel="Select"
|
||||||
@ -451,10 +378,7 @@ const BusinessForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<center>
|
<center>
|
||||||
<Button
|
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5" type="submit">
|
||||||
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Submit application
|
Submit application
|
||||||
</Button>
|
</Button>
|
||||||
</center>
|
</center>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
type IconProps = React.HTMLAttributes<SVGElement>;
|
|
||||||
|
|
||||||
export const Icons = {
|
export const Icons = {
|
||||||
userLogo: (props: IconProps) => (
|
userLogo: () => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
|
|||||||
@ -27,7 +27,8 @@ const ListItem = React.forwardRef<React.ElementRef<"a">, React.ComponentPropsWit
|
|||||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}
|
||||||
|
>
|
||||||
<div className="text-sm font-medium leading-none">{title}</div>
|
<div className="text-sm font-medium leading-none">{title}</div>
|
||||||
<hr />
|
<hr />
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">{children}</p>
|
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">{children}</p>
|
||||||
@ -56,13 +57,6 @@ export function NavigationBar() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const blogComponents = [
|
|
||||||
{
|
|
||||||
title: "Blogs",
|
|
||||||
href: "/landing",
|
|
||||||
description: "Raise on B2DVentures",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 flex flex-wrap w-full bg-card text-sm py-3 border-b-2 border-border z-50">
|
<header className="sticky top-0 flex flex-wrap w-full bg-card text-sm py-3 border-b-2 border-border z-50">
|
||||||
<nav className="max-w-screen-xl w-full mx-auto px-4">
|
<nav className="max-w-screen-xl w-full mx-auto px-4">
|
||||||
@ -71,9 +65,10 @@ export function NavigationBar() {
|
|||||||
<Link
|
<Link
|
||||||
className="flex-none text-xl font-semibold dark:text-white focus:outline-none focus:opacity-80"
|
className="flex-none text-xl font-semibold dark:text-white focus:outline-none focus:opacity-80"
|
||||||
href="/"
|
href="/"
|
||||||
aria-label="Brand">
|
aria-label="Brand"
|
||||||
|
>
|
||||||
<span className="inline-flex items-center gap-x-2 text-xl font-semibold dark:text-white">
|
<span className="inline-flex items-center gap-x-2 text-xl font-semibold dark:text-white">
|
||||||
<Image src="./logo.svg" alt="logo" width={50} height={50} />
|
<Image src="/logo.svg" alt="logo" width={50} height={50} />
|
||||||
B2DVentures
|
B2DVentures
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -108,25 +103,6 @@ export function NavigationBar() {
|
|||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger className="text-base">Blogs</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<ul className="grid w-[400px] ">
|
|
||||||
{blogComponents.map((component) => (
|
|
||||||
<ListItem key={component.title} title={component.title} href={component.href}>
|
|
||||||
{component.description}
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuLink className="text-base font-medium" href="docs">
|
|
||||||
Docs
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
|
|
||||||
<NavigationMenuItem className="pl-5 flex">
|
<NavigationMenuItem className="pl-5 flex">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
@ -16,6 +15,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { Bell, Heart, Wallet } from "lucide-react";
|
import { Bell, Heart, Wallet } from "lucide-react";
|
||||||
import { LogoutButton } from "@/components/auth/logoutButton";
|
import { LogoutButton } from "@/components/auth/logoutButton";
|
||||||
import useSession from "@/lib/supabase/useSession";
|
import useSession from "@/lib/supabase/useSession";
|
||||||
|
import { useUserRole } from "@/hooks/useUserRole";
|
||||||
|
|
||||||
const UnAuthenticatedComponents = () => {
|
const UnAuthenticatedComponents = () => {
|
||||||
return (
|
return (
|
||||||
@ -35,8 +35,13 @@ const UnAuthenticatedComponents = () => {
|
|||||||
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
||||||
let notifications = 1;
|
let notifications = 1;
|
||||||
const displayValue = notifications >= 100 ? "..." : notifications;
|
const displayValue = notifications >= 100 ? "..." : notifications;
|
||||||
|
const { data } = useUserRole();
|
||||||
|
|
||||||
|
const businessClass =
|
||||||
|
data?.role === "business" ? "border-2 border-[#FFD700] bg-[#FFF8DC] dark:bg-[#4B3E2B] rounded-md p-1" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 pl-2 items-center">
|
<div className={`flex gap-3 pl-2 items-center ${businessClass}`}>
|
||||||
<Link href={"/notification"}>
|
<Link href={"/notification"}>
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<Bell className="h-6 w-6 " />
|
<Bell className="h-6 w-6 " />
|
||||||
@ -69,6 +74,11 @@ const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||||
|
{data != null && data != undefined && data.role === "admin" && (
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link href="/admin">Admin</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
|
|||||||
@ -1,25 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Icons } from "./ui/icons";
|
import { Icons } from "./ui/icons";
|
||||||
import { Button } from "./ui/button";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "./ui/card";
|
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||||
import {
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "./ui/select";
|
|
||||||
|
|
||||||
export function CardsPaymentMethod() {
|
export function CardsPaymentMethod() {
|
||||||
return (
|
return (
|
||||||
@ -33,12 +19,7 @@ export function CardsPaymentMethod() {
|
|||||||
<CardContent className="grid gap-6">
|
<CardContent className="grid gap-6">
|
||||||
<RadioGroup defaultValue="card" className="flex w-full justify-center gap-1 md:gap-3">
|
<RadioGroup defaultValue="card" className="flex w-full justify-center gap-1 md:gap-3">
|
||||||
<div className="w-[100px] lg:w-[130px]">
|
<div className="w-[100px] lg:w-[130px]">
|
||||||
<RadioGroupItem
|
<RadioGroupItem value="card" id="card" className="peer sr-only" aria-label="Card" />
|
||||||
value="card"
|
|
||||||
id="card"
|
|
||||||
className="peer sr-only"
|
|
||||||
aria-label="Card"
|
|
||||||
/>
|
|
||||||
<Label
|
<Label
|
||||||
htmlFor="card"
|
htmlFor="card"
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||||
@ -61,12 +42,7 @@ export function CardsPaymentMethod() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[100px] lg:w-[130px]">
|
<div className="w-[100px] lg:w-[130px]">
|
||||||
<RadioGroupItem
|
<RadioGroupItem value="paypal" id="paypal" className="peer sr-only" aria-label="Paypal" />
|
||||||
value="paypal"
|
|
||||||
id="paypal"
|
|
||||||
className="peer sr-only"
|
|
||||||
aria-label="Paypal"
|
|
||||||
/>
|
|
||||||
<Label
|
<Label
|
||||||
htmlFor="paypal"
|
htmlFor="paypal"
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||||
@ -77,12 +53,7 @@ export function CardsPaymentMethod() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[100px] lg:w-[130px]">
|
<div className="w-[100px] lg:w-[130px]">
|
||||||
<RadioGroupItem
|
<RadioGroupItem value="apple" id="apple" className="peer sr-only" aria-label="Apple" />
|
||||||
value="apple"
|
|
||||||
id="apple"
|
|
||||||
className="peer sr-only"
|
|
||||||
aria-label="Apple"
|
|
||||||
/>
|
|
||||||
<Label
|
<Label
|
||||||
htmlFor="apple"
|
htmlFor="apple"
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-transparent p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||||
|
|||||||
@ -27,7 +27,8 @@ export function ProjectCard(props: ProjectCardProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
|
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
|
||||||
props.className
|
props.className
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="relative h-3/4 w-full">
|
<div className="relative h-3/4 w-full">
|
||||||
@ -69,11 +70,15 @@ export function ProjectCard(props: ProjectCardProps) {
|
|||||||
<span className="text-xs">{props.location}</span>
|
<span className="text-xs">{props.location}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap mt-1 items-center text-muted-foreground">
|
<div className="flex flex-wrap mt-1 items-center text-muted-foreground">
|
||||||
{props.tags.map((tag) => (
|
{props.tags && Array.isArray(props.tags) ? (
|
||||||
<span id="tag" key={tag} className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1">
|
props.tags.map((tag) => (
|
||||||
{tag}
|
<span id="tag" key={tag} className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1">
|
||||||
</span>
|
{tag}
|
||||||
))}
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">No tags available</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,26 +1,39 @@
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
||||||
|
export type RecentDealData = {
|
||||||
|
created_time: Date;
|
||||||
|
deal_amount: number;
|
||||||
|
investor_id: string;
|
||||||
|
username: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
// email: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface RecentFundsProps {
|
interface RecentFundsProps {
|
||||||
data?: { name?: string; amount?: number; avatar?: string; date?: Date }[];
|
recentDealData: RecentDealData[];
|
||||||
}
|
}
|
||||||
export function RecentFunds(props: RecentFundsProps) {
|
|
||||||
|
export function RecentFunds({ recentDealData }: RecentFundsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{(props?.data || []).map((deal, index) => (
|
{recentDealData?.length > 0 ? (
|
||||||
<div className="flex items-center" key={index}>
|
recentDealData.map((data) => (
|
||||||
<Avatar className="h-9 w-9">
|
<div className="flex items-center" key={data.investor_id}>
|
||||||
<AvatarImage src={deal.avatar} alt={deal.name} />
|
<Avatar className="h-9 w-9">
|
||||||
<AvatarFallback>{(deal.name ?? "").slice(0, 2)}</AvatarFallback>
|
<AvatarImage src={data.avatar_url} alt={data.username} />
|
||||||
</Avatar>
|
{/* #TODO make this not quick fix */}
|
||||||
<div className="ml-4 space-y-1">
|
<AvatarFallback>{data.username ? data.username[0] : ""}</AvatarFallback>
|
||||||
<p className="text-sm font-medium leading-none">{deal.name}</p>
|
</Avatar>
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="ml-4 space-y-1">
|
||||||
{deal?.date?.toLocaleDateString()}
|
<p className="text-sm font-medium leading-none">{data.username}</p>
|
||||||
</p>
|
{/* <p className="text-sm text-muted-foreground">{data.email}</p> */}
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto font-medium">+${data.deal_amount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto font-medium">+${deal.amount}</div>
|
))
|
||||||
</div>
|
) : (
|
||||||
))}
|
<p>No recent deals available.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/components/ui/alert-dialog.tsx
Normal file
141
src/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
57
src/components/ui/calendar.tsx
Normal file
57
src/components/ui/calendar.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside: "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={
|
||||||
|
{
|
||||||
|
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
|
||||||
|
IconRight: () => <ChevronRight className="h-4 w-4" />,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
742
src/components/ui/datetime-picker.tsx
Normal file
742
src/components/ui/datetime-picker.tsx
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
import type { CalendarProps } from "@/components/ui/calendar";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { add, format } from "date-fns";
|
||||||
|
import { type Locale, enUS } from "date-fns/locale";
|
||||||
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { Clock } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
// ---------- utils start ----------
|
||||||
|
/**
|
||||||
|
* regular expression to check for valid hour format (01-23)
|
||||||
|
*/
|
||||||
|
function isValidHour(value: string) {
|
||||||
|
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* regular expression to check for valid 12 hour format (01-12)
|
||||||
|
*/
|
||||||
|
function isValid12Hour(value: string) {
|
||||||
|
return /^(0[1-9]|1[0-2])$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* regular expression to check for valid minute format (00-59)
|
||||||
|
*/
|
||||||
|
function isValidMinuteOrSecond(value: string) {
|
||||||
|
return /^[0-5][0-9]$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
|
||||||
|
|
||||||
|
function getValidNumber(value: string, { max, min = 0, loop = false }: GetValidNumberConfig) {
|
||||||
|
let numericValue = parseInt(value, 10);
|
||||||
|
|
||||||
|
if (!Number.isNaN(numericValue)) {
|
||||||
|
if (!loop) {
|
||||||
|
if (numericValue > max) numericValue = max;
|
||||||
|
if (numericValue < min) numericValue = min;
|
||||||
|
} else {
|
||||||
|
if (numericValue > max) numericValue = min;
|
||||||
|
if (numericValue < min) numericValue = max;
|
||||||
|
}
|
||||||
|
return numericValue.toString().padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidHour(value: string) {
|
||||||
|
if (isValidHour(value)) return value;
|
||||||
|
return getValidNumber(value, { max: 23 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValid12Hour(value: string) {
|
||||||
|
if (isValid12Hour(value)) return value;
|
||||||
|
return getValidNumber(value, { min: 1, max: 12 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidMinuteOrSecond(value: string) {
|
||||||
|
if (isValidMinuteOrSecond(value)) return value;
|
||||||
|
return getValidNumber(value, { max: 59 });
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValidArrowNumberConfig = {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getValidArrowNumber(value: string, { min, max, step }: GetValidArrowNumberConfig) {
|
||||||
|
let numericValue = parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(numericValue)) {
|
||||||
|
numericValue += step;
|
||||||
|
return getValidNumber(String(numericValue), { min, max, loop: true });
|
||||||
|
}
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidArrowHour(value: string, step: number) {
|
||||||
|
return getValidArrowNumber(value, { min: 0, max: 23, step });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidArrow12Hour(value: string, step: number) {
|
||||||
|
return getValidArrowNumber(value, { min: 1, max: 12, step });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidArrowMinuteOrSecond(value: string, step: number) {
|
||||||
|
return getValidArrowNumber(value, { min: 0, max: 59, step });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMinutes(date: Date, value: string) {
|
||||||
|
const minutes = getValidMinuteOrSecond(value);
|
||||||
|
date.setMinutes(parseInt(minutes, 10));
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSeconds(date: Date, value: string) {
|
||||||
|
const seconds = getValidMinuteOrSecond(value);
|
||||||
|
date.setSeconds(parseInt(seconds, 10));
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHours(date: Date, value: string) {
|
||||||
|
const hours = getValidHour(value);
|
||||||
|
date.setHours(parseInt(hours, 10));
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set12Hours(date: Date, value: string, period: Period) {
|
||||||
|
const hours = parseInt(getValid12Hour(value), 10);
|
||||||
|
const convertedHours = convert12HourTo24Hour(hours, period);
|
||||||
|
date.setHours(convertedHours);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimePickerType = "minutes" | "seconds" | "hours" | "12hours";
|
||||||
|
type Period = "AM" | "PM";
|
||||||
|
|
||||||
|
function setDateByType(date: Date, value: string, type: TimePickerType, period?: Period) {
|
||||||
|
switch (type) {
|
||||||
|
case "minutes":
|
||||||
|
return setMinutes(date, value);
|
||||||
|
case "seconds":
|
||||||
|
return setSeconds(date, value);
|
||||||
|
case "hours":
|
||||||
|
return setHours(date, value);
|
||||||
|
case "12hours": {
|
||||||
|
if (!period) return date;
|
||||||
|
return set12Hours(date, value, period);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateByType(date: Date | null, type: TimePickerType) {
|
||||||
|
if (!date) return "00";
|
||||||
|
switch (type) {
|
||||||
|
case "minutes":
|
||||||
|
return getValidMinuteOrSecond(String(date.getMinutes()));
|
||||||
|
case "seconds":
|
||||||
|
return getValidMinuteOrSecond(String(date.getSeconds()));
|
||||||
|
case "hours":
|
||||||
|
return getValidHour(String(date.getHours()));
|
||||||
|
case "12hours":
|
||||||
|
return getValid12Hour(String(display12HourValue(date.getHours())));
|
||||||
|
default:
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrowByType(value: string, step: number, type: TimePickerType) {
|
||||||
|
switch (type) {
|
||||||
|
case "minutes":
|
||||||
|
return getValidArrowMinuteOrSecond(value, step);
|
||||||
|
case "seconds":
|
||||||
|
return getValidArrowMinuteOrSecond(value, step);
|
||||||
|
case "hours":
|
||||||
|
return getValidArrowHour(value, step);
|
||||||
|
case "12hours":
|
||||||
|
return getValidArrow12Hour(value, step);
|
||||||
|
default:
|
||||||
|
return "00";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles value change of 12-hour input
|
||||||
|
* 12:00 PM is 12:00
|
||||||
|
* 12:00 AM is 00:00
|
||||||
|
*/
|
||||||
|
function convert12HourTo24Hour(hour: number, period: Period) {
|
||||||
|
if (period === "PM") {
|
||||||
|
if (hour <= 11) {
|
||||||
|
return hour + 12;
|
||||||
|
}
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === "AM") {
|
||||||
|
if (hour === 12) return 0;
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* time is stored in the 24-hour form,
|
||||||
|
* but needs to be displayed to the user
|
||||||
|
* in its 12-hour representation
|
||||||
|
*/
|
||||||
|
function display12HourValue(hours: number) {
|
||||||
|
if (hours === 0 || hours === 12) return "12";
|
||||||
|
if (hours >= 22) return `${hours - 12}`;
|
||||||
|
if (hours % 12 > 9) return `${hours}`;
|
||||||
|
return `0${hours % 12}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genMonths(locale: Pick<Locale, "options" | "localize" | "formatLong">) {
|
||||||
|
return Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
value: i,
|
||||||
|
label: format(new Date(2021, i), "MMMM", { locale }),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function genYears(yearRange = 50) {
|
||||||
|
const today = new Date();
|
||||||
|
return Array.from({ length: yearRange * 2 + 1 }, (_, i) => ({
|
||||||
|
value: today.getFullYear() - yearRange + i,
|
||||||
|
label: (today.getFullYear() - yearRange + i).toString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- utils end ----------
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
yearRange = 50,
|
||||||
|
...props
|
||||||
|
}: CalendarProps & { yearRange?: number }) {
|
||||||
|
const MONTHS = React.useMemo(() => {
|
||||||
|
let locale: Pick<Locale, "options" | "localize" | "formatLong"> = enUS;
|
||||||
|
const { options, localize, formatLong } = props.locale || {};
|
||||||
|
if (options && localize && formatLong) {
|
||||||
|
locale = {
|
||||||
|
options,
|
||||||
|
localize,
|
||||||
|
formatLong,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return genMonths(locale);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const YEARS = React.useMemo(() => genYears(yearRange), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-y-0 justify-center",
|
||||||
|
month: "flex flex-col items-center space-y-4",
|
||||||
|
month_caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center ",
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-5 top-5"
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-5 top-5"
|
||||||
|
),
|
||||||
|
month_grid: "w-full border-collapse space-y-1",
|
||||||
|
weekdays: cn("flex", props.showWeekNumber && "justify-end"),
|
||||||
|
weekday: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
week: "flex w-full mt-2",
|
||||||
|
day: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20 rounded-1",
|
||||||
|
day_button: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100 rounded-l-md rounded-r-md"
|
||||||
|
),
|
||||||
|
range_end: "day-range-end",
|
||||||
|
selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground rounded-l-md rounded-r-md",
|
||||||
|
today: "bg-accent text-accent-foreground",
|
||||||
|
outside:
|
||||||
|
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||||
|
disabled: "text-muted-foreground opacity-50",
|
||||||
|
range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Chevron: ({ ...props }) =>
|
||||||
|
props.orientation === "left" ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />,
|
||||||
|
MonthCaption: ({ calendarMonth }) => {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex gap-2">
|
||||||
|
<Select
|
||||||
|
defaultValue={calendarMonth.date.getMonth().toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newDate = new Date(calendarMonth.date);
|
||||||
|
newDate.setMonth(Number.parseInt(value, 10));
|
||||||
|
props.onMonthChange?.(newDate);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-fit gap-1 border-none p-0 focus:bg-accent focus:text-accent-foreground">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{MONTHS.map((month) => (
|
||||||
|
<SelectItem key={month.value} value={month.value.toString()}>
|
||||||
|
{month.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
defaultValue={calendarMonth.date.getFullYear().toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newDate = new Date(calendarMonth.date);
|
||||||
|
newDate.setFullYear(Number.parseInt(value, 10));
|
||||||
|
props.onMonthChange?.(newDate);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-fit gap-1 border-none p-0 focus:bg-accent focus:text-accent-foreground">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{YEARS.map((year) => (
|
||||||
|
<SelectItem key={year.value} value={year.value.toString()}>
|
||||||
|
{year.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
interface PeriodSelectorProps {
|
||||||
|
period: Period;
|
||||||
|
setPeriod?: (m: Period) => void;
|
||||||
|
date?: Date | null;
|
||||||
|
onDateChange?: (date: Date | undefined) => void;
|
||||||
|
onRightFocus?: () => void;
|
||||||
|
onLeftFocus?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimePeriodSelect = React.forwardRef<HTMLButtonElement, PeriodSelectorProps>(
|
||||||
|
({ period, setPeriod, date, onDateChange, onLeftFocus, onRightFocus }, ref) => {
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (e.key === "ArrowRight") onRightFocus?.();
|
||||||
|
if (e.key === "ArrowLeft") onLeftFocus?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueChange = (value: Period) => {
|
||||||
|
setPeriod?.(value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* trigger an update whenever the user switches between AM and PM;
|
||||||
|
* otherwise user must manually change the hour each time
|
||||||
|
*/
|
||||||
|
if (date) {
|
||||||
|
const tempDate = new Date(date);
|
||||||
|
const hours = display12HourValue(date.getHours());
|
||||||
|
onDateChange?.(setDateByType(tempDate, hours.toString(), "12hours", period === "AM" ? "PM" : "AM"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-10 items-center">
|
||||||
|
<Select defaultValue={period} onValueChange={(value: Period) => handleValueChange(value)}>
|
||||||
|
<SelectTrigger
|
||||||
|
ref={ref}
|
||||||
|
className="w-[65px] focus:bg-accent focus:text-accent-foreground"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="AM">AM</SelectItem>
|
||||||
|
<SelectItem value="PM">PM</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TimePeriodSelect.displayName = "TimePeriodSelect";
|
||||||
|
|
||||||
|
interface TimePickerInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
picker: TimePickerType;
|
||||||
|
date?: Date | null;
|
||||||
|
onDateChange?: (date: Date | undefined) => void;
|
||||||
|
period?: Period;
|
||||||
|
onRightFocus?: () => void;
|
||||||
|
onLeftFocus?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimePickerInput = React.forwardRef<HTMLInputElement, TimePickerInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
type = "tel",
|
||||||
|
value,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
date = new Date(new Date().setHours(0, 0, 0, 0)),
|
||||||
|
onDateChange,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
picker,
|
||||||
|
period,
|
||||||
|
onLeftFocus,
|
||||||
|
onRightFocus,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [flag, setFlag] = React.useState<boolean>(false);
|
||||||
|
const [prevIntKey, setPrevIntKey] = React.useState<string>("0");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* allow the user to enter the second digit within 2 seconds
|
||||||
|
* otherwise start again with entering first digit
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (flag) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setFlag(false);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [flag]);
|
||||||
|
|
||||||
|
const calculatedValue = React.useMemo(() => {
|
||||||
|
return getDateByType(date, picker);
|
||||||
|
}, [date, picker]);
|
||||||
|
|
||||||
|
const calculateNewValue = (key: string) => {
|
||||||
|
/*
|
||||||
|
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
|
||||||
|
* The second entered digit will break the condition and the value will be set to 10-12.
|
||||||
|
*/
|
||||||
|
if (picker === "12hours") {
|
||||||
|
if (flag && calculatedValue.slice(1, 2) === "1" && prevIntKey === "0") return `0${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Tab") return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.key === "ArrowRight") onRightFocus?.();
|
||||||
|
if (e.key === "ArrowLeft") onLeftFocus?.();
|
||||||
|
if (["ArrowUp", "ArrowDown"].includes(e.key)) {
|
||||||
|
const step = e.key === "ArrowUp" ? 1 : -1;
|
||||||
|
const newValue = getArrowByType(calculatedValue, step, picker);
|
||||||
|
if (flag) setFlag(false);
|
||||||
|
const tempDate = date ? new Date(date) : new Date();
|
||||||
|
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
|
||||||
|
}
|
||||||
|
if (e.key >= "0" && e.key <= "9") {
|
||||||
|
if (picker === "12hours") setPrevIntKey(e.key);
|
||||||
|
|
||||||
|
const newValue = calculateNewValue(e.key);
|
||||||
|
if (flag) onRightFocus?.();
|
||||||
|
setFlag((prev) => !prev);
|
||||||
|
const tempDate = date ? new Date(date) : new Date();
|
||||||
|
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
id={id || picker}
|
||||||
|
name={name || picker}
|
||||||
|
className={cn(
|
||||||
|
"w-[48px] text-center font-mono text-base tabular-nums caret-transparent focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
value={value || calculatedValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onChange?.(e);
|
||||||
|
}}
|
||||||
|
type={type}
|
||||||
|
inputMode="decimal"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
onKeyDown?.(e);
|
||||||
|
handleKeyDown(e);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TimePickerInput.displayName = "TimePickerInput";
|
||||||
|
|
||||||
|
interface TimePickerProps {
|
||||||
|
date?: Date | null;
|
||||||
|
onChange?: (date: Date | undefined) => void;
|
||||||
|
hourCycle?: 12 | 24;
|
||||||
|
/**
|
||||||
|
* Determines the smallest unit that is displayed in the datetime picker.
|
||||||
|
* Default is 'second'.
|
||||||
|
* */
|
||||||
|
granularity?: Granularity;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimePickerRef {
|
||||||
|
minuteRef: HTMLInputElement | null;
|
||||||
|
hourRef: HTMLInputElement | null;
|
||||||
|
secondRef: HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimePicker = React.forwardRef<TimePickerRef, TimePickerProps>(
|
||||||
|
({ date, onChange, hourCycle = 24, granularity = "second" }, ref) => {
|
||||||
|
const minuteRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const hourRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const secondRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const periodRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
const [period, setPeriod] = React.useState<Period>(date && date.getHours() >= 12 ? "PM" : "AM");
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
minuteRef: minuteRef.current,
|
||||||
|
hourRef: hourRef.current,
|
||||||
|
secondRef: secondRef.current,
|
||||||
|
periodRef: periodRef.current,
|
||||||
|
}),
|
||||||
|
[minuteRef, hourRef, secondRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<label htmlFor="datetime-picker-hour-input" className="cursor-pointer">
|
||||||
|
<Clock className="mr-2 h-4 w-4" />
|
||||||
|
</label>
|
||||||
|
<TimePickerInput
|
||||||
|
picker={hourCycle === 24 ? "hours" : "12hours"}
|
||||||
|
date={date}
|
||||||
|
id="datetime-picker-hour-input"
|
||||||
|
onDateChange={onChange}
|
||||||
|
ref={hourRef}
|
||||||
|
period={period}
|
||||||
|
onRightFocus={() => minuteRef?.current?.focus()}
|
||||||
|
/>
|
||||||
|
{(granularity === "minute" || granularity === "second") && (
|
||||||
|
<>
|
||||||
|
:
|
||||||
|
<TimePickerInput
|
||||||
|
picker="minutes"
|
||||||
|
date={date}
|
||||||
|
onDateChange={onChange}
|
||||||
|
ref={minuteRef}
|
||||||
|
onLeftFocus={() => hourRef?.current?.focus()}
|
||||||
|
onRightFocus={() => secondRef?.current?.focus()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{granularity === "second" && (
|
||||||
|
<>
|
||||||
|
:
|
||||||
|
<TimePickerInput
|
||||||
|
picker="seconds"
|
||||||
|
date={date}
|
||||||
|
onDateChange={onChange}
|
||||||
|
ref={secondRef}
|
||||||
|
onLeftFocus={() => minuteRef?.current?.focus()}
|
||||||
|
onRightFocus={() => periodRef?.current?.focus()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{hourCycle === 12 && (
|
||||||
|
<div className="grid gap-1 text-center">
|
||||||
|
<TimePeriodSelect
|
||||||
|
period={period}
|
||||||
|
setPeriod={setPeriod}
|
||||||
|
date={date}
|
||||||
|
onDateChange={(date) => {
|
||||||
|
onChange?.(date);
|
||||||
|
if (date && date?.getHours() >= 12) {
|
||||||
|
setPeriod("PM");
|
||||||
|
} else {
|
||||||
|
setPeriod("AM");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={periodRef}
|
||||||
|
onLeftFocus={() => secondRef?.current?.focus()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
TimePicker.displayName = "TimePicker";
|
||||||
|
|
||||||
|
type Granularity = "day" | "hour" | "minute" | "second";
|
||||||
|
|
||||||
|
type DateTimePickerProps = {
|
||||||
|
value?: Date;
|
||||||
|
onChange?: (date: Date | undefined) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
/** showing `AM/PM` or not. */
|
||||||
|
hourCycle?: 12 | 24;
|
||||||
|
placeholder?: string;
|
||||||
|
/**
|
||||||
|
* The year range will be: `This year + yearRange` and `this year - yearRange`.
|
||||||
|
* Default is 50.
|
||||||
|
* For example:
|
||||||
|
* This year is 2024, The year dropdown will be 1974 to 2024 which is generated by `2024 - 50 = 1974` and `2024 + 50 = 2074`.
|
||||||
|
* */
|
||||||
|
yearRange?: number;
|
||||||
|
/**
|
||||||
|
* The format is derived from the `date-fns` documentation.
|
||||||
|
* @reference https://date-fns.org/v3.6.0/docs/format
|
||||||
|
**/
|
||||||
|
displayFormat?: { hour24?: string; hour12?: string };
|
||||||
|
/**
|
||||||
|
* The granularity prop allows you to control the smallest unit that is displayed by DateTimePicker.
|
||||||
|
* By default, the value is `second` which shows all time inputs.
|
||||||
|
**/
|
||||||
|
granularity?: Granularity;
|
||||||
|
className?: string;
|
||||||
|
} & Pick<CalendarProps, "locale" | "weekStartsOn" | "showWeekNumber" | "showOutsideDays">;
|
||||||
|
|
||||||
|
type DateTimePickerRef = {
|
||||||
|
value?: Date;
|
||||||
|
} & Omit<HTMLButtonElement, "value">;
|
||||||
|
|
||||||
|
const DateTimePicker = React.forwardRef<Partial<DateTimePickerRef>, DateTimePickerProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
locale = enUS,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
hourCycle = 24,
|
||||||
|
yearRange = 50,
|
||||||
|
disabled = false,
|
||||||
|
displayFormat,
|
||||||
|
granularity = "second",
|
||||||
|
placeholder = "Pick a date",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [month, setMonth] = React.useState<Date>(value ?? new Date());
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
/**
|
||||||
|
* carry over the current time when a user clicks a new day
|
||||||
|
* instead of resetting to 00:00
|
||||||
|
*/
|
||||||
|
const handleSelect = (newDay: Date | undefined) => {
|
||||||
|
if (!newDay) return;
|
||||||
|
if (!value) {
|
||||||
|
onChange?.(newDay);
|
||||||
|
setMonth(newDay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const diff = newDay.getTime() - value.getTime();
|
||||||
|
const diffInDays = diff / (1000 * 60 * 60 * 24);
|
||||||
|
const newDateFull = add(value, { days: Math.ceil(diffInDays) });
|
||||||
|
onChange?.(newDateFull);
|
||||||
|
setMonth(newDateFull);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
...buttonRef.current,
|
||||||
|
value,
|
||||||
|
}),
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const initHourFormat = {
|
||||||
|
hour24: displayFormat?.hour24 ?? `PPP HH:mm${!granularity || granularity === "second" ? ":ss" : ""}`,
|
||||||
|
hour12: displayFormat?.hour12 ?? `PP hh:mm${!granularity || granularity === "second" ? ":ss" : ""} b`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let loc = enUS;
|
||||||
|
const { options, localize, formatLong } = locale;
|
||||||
|
if (options && localize && formatLong) {
|
||||||
|
loc = {
|
||||||
|
...enUS,
|
||||||
|
options,
|
||||||
|
localize,
|
||||||
|
formatLong,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild disabled={disabled}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn("w-full justify-start text-left font-normal", !value && "text-muted-foreground", className)}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{value ? (
|
||||||
|
format(value, hourCycle === 24 ? initHourFormat.hour24 : initHourFormat.hour12, {
|
||||||
|
locale: loc,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span>{placeholder}</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={value}
|
||||||
|
month={month}
|
||||||
|
onSelect={(d) => handleSelect(d)}
|
||||||
|
onMonthChange={handleSelect}
|
||||||
|
yearRange={yearRange}
|
||||||
|
locale={locale}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{granularity !== "day" && (
|
||||||
|
<div className="border-t border-border p-3">
|
||||||
|
<TimePicker onChange={onChange} date={value} hourCycle={hourCycle} granularity={granularity} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DateTimePicker.displayName = "DateTimePicker";
|
||||||
|
|
||||||
|
export { DateTimePicker, TimePickerInput, TimePicker };
|
||||||
|
export type { TimePickerType, DateTimePickerProps, DateTimePickerRef };
|
||||||
@ -1,138 +1,46 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, LineChart, Line, Tooltip } from "recharts";
|
||||||
Bar,
|
|
||||||
BarChart,
|
|
||||||
ResponsiveContainer,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
Tooltip,
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
// const data = [
|
|
||||||
// {
|
|
||||||
// name: "Jan",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Feb",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Mar",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Apr",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "May",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Jun",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Jul",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Aug",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Sep",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Oct",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Nov",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Dec",
|
|
||||||
// total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
interface OverViewProps {
|
interface OverViewProps {
|
||||||
graphType: string;
|
graphType: string;
|
||||||
data: { name: string; value: number }[];
|
graphData: Record<string, number>; // Object with month-year as keys and sum as value
|
||||||
graphHeight?: number | string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Overview(props: OverViewProps) {
|
export function Overview(props: OverViewProps) {
|
||||||
|
// Transform the grouped data into the format for the chart
|
||||||
|
const chartData = Object.entries(props.graphData).map(([monthYear, totalArray]) => ({
|
||||||
|
name: monthYear,
|
||||||
|
total: totalArray, // Get the total amount for the month
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={props.graphHeight || 350}>
|
<ResponsiveContainer width="100%" height={350}>
|
||||||
{props.graphType === "line" ? (
|
{props.graphType === "line" ? (
|
||||||
<LineChart data={props.data}>
|
<LineChart data={chartData}>
|
||||||
<XAxis
|
<XAxis dataKey="name" stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
||||||
dataKey="name"
|
<YAxis
|
||||||
stroke="#888888"
|
stroke="#888888"
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
tickFormatter={(value) => `$${value}`}
|
||||||
<YAxis
|
/>
|
||||||
stroke="#888888"
|
<Line dataKey="total" fill="currentColor" className="fill-primary" />
|
||||||
fontSize={12}
|
</LineChart>
|
||||||
tickLine={false}
|
) : (
|
||||||
axisLine={false}
|
<BarChart data={chartData}>
|
||||||
tickFormatter={(value) => `$${value}`}
|
<XAxis dataKey="name" stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
||||||
/>
|
<YAxis
|
||||||
<Tooltip
|
stroke="#888888"
|
||||||
formatter={(value) => `$${value}`}
|
fontSize={12}
|
||||||
contentStyle={{
|
tickLine={false}
|
||||||
backgroundColor: "#f5f5f5",
|
axisLine={false}
|
||||||
borderRadius: "5px",
|
tickFormatter={(value) => `$${value}`}
|
||||||
color: "#000",
|
/>
|
||||||
}}
|
<Bar dataKey="total" fill="currentColor" className="fill-primary" />
|
||||||
/>
|
</BarChart>
|
||||||
<Line
|
)}
|
||||||
dataKey="value"
|
</ResponsiveContainer>
|
||||||
fill="currentColor"
|
|
||||||
className="fill-primary"
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
) : (
|
|
||||||
<BarChart data={props.data}>
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
stroke="#888888"
|
|
||||||
fontSize={12}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
stroke="#888888"
|
|
||||||
fontSize={12}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => `$${value}`}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={(value) => `$${value}`}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: "#f5f5f5",
|
|
||||||
borderRadius: "5px",
|
|
||||||
color: "#000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="value"
|
|
||||||
fill="currentColor"
|
|
||||||
className="fill-primary"
|
|
||||||
radius={[15, 15, 0, 0]}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
)}
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/hooks/useDealList.ts
Normal file
0
src/hooks/useDealList.ts
Normal file
41
src/hooks/useUserRole.ts
Normal file
41
src/hooks/useUserRole.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { getUserRole } from "@/lib/data/userQuery";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Session } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export function useUserRole() {
|
||||||
|
const client = createSupabaseClient();
|
||||||
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const [sessionError, setSessionError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchSession() {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
error,
|
||||||
|
} = await client.auth.getSession();
|
||||||
|
if (error) throw error;
|
||||||
|
setSession(session);
|
||||||
|
} catch (error) {
|
||||||
|
setSessionError(error as Error);
|
||||||
|
console.error("Error fetching session:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSession();
|
||||||
|
}, [client.auth]);
|
||||||
|
|
||||||
|
const { data, isLoading, error: userRoleError } = useQuery(getUserRole(client, session?.user?.id!));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: isLoading || !session,
|
||||||
|
error: userRoleError || sessionError,
|
||||||
|
session,
|
||||||
|
userId: session?.user?.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
37
src/lib/data/applicationMutate.ts
Normal file
37
src/lib/data/applicationMutate.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export async function rejectBusiness(client: SupabaseClient, businessApplicationId: Number) {
|
||||||
|
return client
|
||||||
|
.from("business_application")
|
||||||
|
.update({
|
||||||
|
status: "reject",
|
||||||
|
})
|
||||||
|
.eq("id", businessApplicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function approveBusiness(client: SupabaseClient, businessApplicationId: Number) {
|
||||||
|
return client
|
||||||
|
.from("business_application")
|
||||||
|
.update({
|
||||||
|
status: "approve",
|
||||||
|
})
|
||||||
|
.eq("id", businessApplicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rejectProject(client: SupabaseClient, projectApplicationId: Number) {
|
||||||
|
return client
|
||||||
|
.from("project_application")
|
||||||
|
.update({
|
||||||
|
status: "reject",
|
||||||
|
})
|
||||||
|
.eq("id", projectApplicationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function approveProject(client: SupabaseClient, projectApplicationId: Number) {
|
||||||
|
return client
|
||||||
|
.from("project_application")
|
||||||
|
.update({
|
||||||
|
status: "approve",
|
||||||
|
})
|
||||||
|
.eq("id", projectApplicationId);
|
||||||
|
}
|
||||||
59
src/lib/data/applicationQuery.ts
Normal file
59
src/lib/data/applicationQuery.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const getAllBusinessApplicationQuery = (client: SupabaseClient) => {
|
||||||
|
return client.from("business_application").select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
...user_id!inner (
|
||||||
|
user_id:id,
|
||||||
|
username
|
||||||
|
),
|
||||||
|
...business_type_id!inner (
|
||||||
|
business_type_id:id,
|
||||||
|
business_type_value:value
|
||||||
|
),
|
||||||
|
project_application_id,
|
||||||
|
business_name,
|
||||||
|
created_at,
|
||||||
|
is_in_us,
|
||||||
|
is_for_sale,
|
||||||
|
pitch_deck_url,
|
||||||
|
community_size,
|
||||||
|
is_generating_revenue,
|
||||||
|
money_raised_to_date,
|
||||||
|
location,
|
||||||
|
status
|
||||||
|
`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllProjectApplicationByBusinessQuery = (client: SupabaseClient, businessId: number) => {
|
||||||
|
return client
|
||||||
|
.from("project_application")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
deadline,
|
||||||
|
status,
|
||||||
|
project_name,
|
||||||
|
...business_id (
|
||||||
|
business_id:id,
|
||||||
|
business_name,
|
||||||
|
user_id
|
||||||
|
),
|
||||||
|
...project_type_id!inner (
|
||||||
|
project_type_id:id,
|
||||||
|
project_type_value:value
|
||||||
|
),
|
||||||
|
short_description,
|
||||||
|
pitch_deck_url,
|
||||||
|
project_logo,
|
||||||
|
min_investment,
|
||||||
|
target_investment,
|
||||||
|
user_id,
|
||||||
|
project_photos
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq("business_id", businessId);
|
||||||
|
};
|
||||||
29
src/lib/data/bucket/deleteFile.ts
Normal file
29
src/lib/data/bucket/deleteFile.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import { Database } from "@/types/database.types";
|
||||||
|
|
||||||
|
export async function deleteFileFromDataRoom(
|
||||||
|
supabase: SupabaseClient<Database>,
|
||||||
|
dataroomId: number,
|
||||||
|
fileName: string,
|
||||||
|
dataroomMaterialId: number
|
||||||
|
) {
|
||||||
|
const filePath = `${dataroomId}/${fileName}`;
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error("Invalid filepath: Unable to extract file path for deletion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: storageError } = await supabase.storage.from("dataroom-material").remove([filePath]);
|
||||||
|
|
||||||
|
if (storageError) {
|
||||||
|
throw new Error(`Error deleting file from storage: ${storageError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: dbError } = await supabase.from("dataroom_material").delete().eq("id", dataroomMaterialId);
|
||||||
|
|
||||||
|
if (dbError) {
|
||||||
|
throw new Error(`Error deleting file from database: ${dbError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
31
src/lib/data/bucket/uploadAvatar.ts
Normal file
31
src/lib/data/bucket/uploadAvatar.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export async function uploadAvatar(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
file: File,
|
||||||
|
uid: string,
|
||||||
|
) {
|
||||||
|
const allowedExtensions = ["jpeg", "jpg", "png"];
|
||||||
|
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid file format. Only jpeg, jpg, and png are allowed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `profile-${uid}.${fileExtension}`;
|
||||||
|
|
||||||
|
const { data, error } = await supabase.storage
|
||||||
|
.from("avatars")
|
||||||
|
.upload(fileName, file, {
|
||||||
|
upsert: true,
|
||||||
|
contentType: `image/${fileExtension}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
54
src/lib/data/bucket/uploadFile.ts
Normal file
54
src/lib/data/bucket/uploadFile.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export async function uploadFileToDataRoom(supabase: SupabaseClient, file: File, dataRoomId: number) {
|
||||||
|
const allowedExtensions = ["pdf", "docx", "xlsx", "pptx", "txt"];
|
||||||
|
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||||
|
throw new Error("Invalid file format. Only pdf, docx, xlsx, pptx, and txt are allowed.");
|
||||||
|
}
|
||||||
|
const { data: fileTypeData, error: fileTypeError } = await supabase
|
||||||
|
.from("material_file_type")
|
||||||
|
.select("id")
|
||||||
|
.eq("value", fileExtension)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fileTypeError || !fileTypeData) {
|
||||||
|
throw new Error("File type not supported or not found in material_file_type table.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileTypeId = fileTypeData.id;
|
||||||
|
|
||||||
|
const fileName = `${dataRoomId}/${file.name}`;
|
||||||
|
const { data: uploadData, error: uploadError } = await supabase.storage
|
||||||
|
.from("dataroom-material")
|
||||||
|
.upload(fileName, file, {
|
||||||
|
upsert: true,
|
||||||
|
contentType: file.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploadError) {
|
||||||
|
throw uploadError;
|
||||||
|
}
|
||||||
|
const { data: signedUrlData, error: signedUrlError } = await supabase.storage
|
||||||
|
.from("dataroom-material")
|
||||||
|
.createSignedUrl(fileName, 2628000);
|
||||||
|
|
||||||
|
if (signedUrlError) {
|
||||||
|
throw signedUrlError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: insertError } = await supabase.from("dataroom_material").insert([
|
||||||
|
{
|
||||||
|
dataroom_id: dataRoomId,
|
||||||
|
file_url: signedUrlData.signedUrl,
|
||||||
|
file_type_id: fileTypeId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
throw insertError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadData;
|
||||||
|
}
|
||||||
68
src/lib/data/businessQuery.ts
Normal file
68
src/lib/data/businessQuery.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const getAllBusinesses = (client: SupabaseClient) => {
|
||||||
|
return client.from("business").select(`
|
||||||
|
id,
|
||||||
|
location,
|
||||||
|
business_name,
|
||||||
|
...business_type (
|
||||||
|
business_type:value
|
||||||
|
),
|
||||||
|
joined_date,
|
||||||
|
...user_id (
|
||||||
|
user_id:id,
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBusinessAndProject = (
|
||||||
|
client: SupabaseClient,
|
||||||
|
params: { businessName?: String | null; businessId?: number | null; single?: boolean } = { single: false }
|
||||||
|
) => {
|
||||||
|
const query = client.from("business").select(`
|
||||||
|
business_id:id,
|
||||||
|
location,
|
||||||
|
business_name,
|
||||||
|
business_type:business_type (
|
||||||
|
business_type_id:id,
|
||||||
|
value
|
||||||
|
),
|
||||||
|
joined_date,
|
||||||
|
user_id,
|
||||||
|
projects:project (
|
||||||
|
id,
|
||||||
|
project_name,
|
||||||
|
business_id,
|
||||||
|
published_time,
|
||||||
|
card_image_url,
|
||||||
|
project_short_description,
|
||||||
|
...project_investment_detail (
|
||||||
|
min_investment,
|
||||||
|
total_investment,
|
||||||
|
target_investment
|
||||||
|
),
|
||||||
|
tags:project_tag (
|
||||||
|
...tag (
|
||||||
|
tag_value:value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (params.businessName && params.businessName.trim() !== "") {
|
||||||
|
return query.ilike("business_name", `%${params.businessName}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.businessId) {
|
||||||
|
query.eq("id", params.businessId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.single) {
|
||||||
|
query.single();
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
19
src/lib/data/dataroomMutate.ts
Normal file
19
src/lib/data/dataroomMutate.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const requestAccessToDataRoom = (client: SupabaseClient, dataRoomId: number, userId: string) => {
|
||||||
|
return client.from("access_request").insert([
|
||||||
|
{
|
||||||
|
dataroom_id: dataRoomId,
|
||||||
|
user_id: userId,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAccessRequestStatus = (
|
||||||
|
client: SupabaseClient,
|
||||||
|
requestId: number,
|
||||||
|
status: "approve" | "reject" | "pending"
|
||||||
|
) => {
|
||||||
|
return client.from("access_request").update({ status: status }).eq("id", requestId);
|
||||||
|
};
|
||||||
70
src/lib/data/dataroomQuery.ts
Normal file
70
src/lib/data/dataroomQuery.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Database } from "@/types/database.types";
|
||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const getFilesByDataroomId = (client: SupabaseClient<Database>, dataroomId: number) => {
|
||||||
|
const query = client
|
||||||
|
.from("dataroom_material")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
dataroom_id,
|
||||||
|
file_url,
|
||||||
|
file_type:material_file_type!inner (
|
||||||
|
id,
|
||||||
|
value
|
||||||
|
),
|
||||||
|
uploaded_at
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq("dataroom_id", dataroomId);
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDataRoomsByProjectId = (client: SupabaseClient, projectId: number) => {
|
||||||
|
return client
|
||||||
|
.from("dataroom")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
is_public,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
dataroom_material (
|
||||||
|
id,
|
||||||
|
file_url,
|
||||||
|
...file_type_id (
|
||||||
|
file_type_id:id,
|
||||||
|
file_type_value:value
|
||||||
|
),
|
||||||
|
uploaded_at
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq("project_id", projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccessRequests = (
|
||||||
|
client: SupabaseClient<Database>,
|
||||||
|
filters: { dataroomId?: number; userId?: string }
|
||||||
|
) => {
|
||||||
|
let query = client.from("access_request").select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
dataroom_id,
|
||||||
|
user_id,
|
||||||
|
status,
|
||||||
|
requested_at
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filters.dataroomId !== undefined) {
|
||||||
|
query = query.eq("dataroom_id", filters.dataroomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.userId !== undefined) {
|
||||||
|
query = query.eq("user_id", filters.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
31
src/lib/data/investmentQuery.ts
Normal file
31
src/lib/data/investmentQuery.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const getInvestmentCountsByProjectsIds = (client: SupabaseClient, projectIds: string[]) => {
|
||||||
|
return client
|
||||||
|
.from("investment_deal")
|
||||||
|
.select("*", {
|
||||||
|
count: "exact",
|
||||||
|
head: true,
|
||||||
|
})
|
||||||
|
.in("project_id", projectIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInvestmentByUserId = (client: SupabaseClient, userId: string) => {
|
||||||
|
return client
|
||||||
|
.from("investment_deal")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
...project_id (
|
||||||
|
project_id:id,
|
||||||
|
project_name,
|
||||||
|
project_short_description,
|
||||||
|
dataroom_id
|
||||||
|
),
|
||||||
|
deal_amount,
|
||||||
|
investor_id,
|
||||||
|
created_time
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq("investor_id", userId);
|
||||||
|
};
|
||||||
48
src/lib/data/profileMutate.ts
Normal file
48
src/lib/data/profileMutate.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
interface UpdateData {
|
||||||
|
username?: string;
|
||||||
|
full_name?: string;
|
||||||
|
bio?: string;
|
||||||
|
updated_at?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProfile(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
userId: string,
|
||||||
|
updates: UpdateData,
|
||||||
|
) {
|
||||||
|
const updateData: { [key: string]: any | undefined } = {};
|
||||||
|
|
||||||
|
if (updates.username || updates.username != "") {
|
||||||
|
updateData.username = updates.username;
|
||||||
|
}
|
||||||
|
if (updates.full_name || updates.full_name != "") {
|
||||||
|
updateData.full_name = updates.full_name;
|
||||||
|
}
|
||||||
|
if (updates.bio || updates.bio != "") {
|
||||||
|
updateData.bio = updates.bio;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData.updated_at = new Date();
|
||||||
|
|
||||||
|
if (
|
||||||
|
updateData.username != undefined || updateData.full_name != undefined ||
|
||||||
|
updateData.bio != undefined
|
||||||
|
) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.update(updateData)
|
||||||
|
.eq("id", userId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating profile:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log("No fields to update.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,36 +1,33 @@
|
|||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
async function getTopProjects(
|
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
||||||
client: SupabaseClient,
|
|
||||||
numberOfRecords: number = 4
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from("project")
|
.from("project")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,
|
id,
|
||||||
project_name,
|
project_name,
|
||||||
business_id,
|
business_id,
|
||||||
published_time,
|
published_time,
|
||||||
project_short_description,
|
project_short_description,
|
||||||
project_logo,
|
card_image_url,
|
||||||
project_investment_detail (
|
project_investment_detail (
|
||||||
min_investment,
|
min_investment,
|
||||||
total_investment,
|
total_investment,
|
||||||
target_investment,
|
target_investment,
|
||||||
investment_deadline
|
investment_deadline
|
||||||
),
|
),
|
||||||
project_tag (
|
project_tag (
|
||||||
tag (
|
tag (
|
||||||
id,
|
id,
|
||||||
value
|
value
|
||||||
)
|
|
||||||
),
|
|
||||||
business (
|
|
||||||
location
|
|
||||||
)
|
)
|
||||||
`
|
),
|
||||||
|
business (
|
||||||
|
location
|
||||||
|
)
|
||||||
|
`
|
||||||
)
|
)
|
||||||
.order("published_time", { ascending: false })
|
.order("published_time", { ascending: false })
|
||||||
.limit(numberOfRecords);
|
.limit(numberOfRecords);
|
||||||
@ -62,7 +59,7 @@ function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
|||||||
target_investment,
|
target_investment,
|
||||||
investment_deadline
|
investment_deadline
|
||||||
),
|
),
|
||||||
tags:item_tag!inner (
|
tags:project_tag!inner (
|
||||||
...tag!inner (
|
...tag!inner (
|
||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
@ -82,6 +79,7 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
|||||||
project_short_description,
|
project_short_description,
|
||||||
project_description,
|
project_description,
|
||||||
published_time,
|
published_time,
|
||||||
|
card_image_url,
|
||||||
...project_investment_detail!inner (
|
...project_investment_detail!inner (
|
||||||
min_investment,
|
min_investment,
|
||||||
total_investment,
|
total_investment,
|
||||||
@ -92,6 +90,9 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
|||||||
...tag!inner (
|
...tag!inner (
|
||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
...business (
|
||||||
|
user_id
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@ -129,7 +130,7 @@ function searchProjectsQuery(
|
|||||||
}: FilterProjectQueryParams
|
}: FilterProjectQueryParams
|
||||||
) {
|
) {
|
||||||
const start = (page - 1) * pageSize;
|
const start = (page - 1) * pageSize;
|
||||||
const end = start + pageSize - 1;
|
const end = start + pageSize;
|
||||||
|
|
||||||
let query = client
|
let query = client
|
||||||
.from("project")
|
.from("project")
|
||||||
@ -149,7 +150,7 @@ function searchProjectsQuery(
|
|||||||
target_investment,
|
target_investment,
|
||||||
investment_deadline
|
investment_deadline
|
||||||
),
|
),
|
||||||
tags:item_tag!inner (
|
tags:project_tag!inner (
|
||||||
...tag!inner (
|
...tag!inner (
|
||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
@ -186,7 +187,7 @@ function searchProjectsQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tagsFilter) {
|
if (tagsFilter) {
|
||||||
query = query.in("item_tag.tag.value", tagsFilter);
|
query = query.in("project_tag.tag.value", tagsFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectStatus) {
|
if (projectStatus) {
|
||||||
@ -200,9 +201,48 @@ function searchProjectsQuery(
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProjectByBusinessId = (client: SupabaseClient, businessIds: string[]) => {
|
||||||
|
return client
|
||||||
|
.from("project")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
project_name,
|
||||||
|
business_id,
|
||||||
|
published_time,
|
||||||
|
card_image_url,
|
||||||
|
project_short_description,
|
||||||
|
...project_investment_detail (
|
||||||
|
min_investment,
|
||||||
|
total_investment,
|
||||||
|
target_investment
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.in("business_id", businessIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectByUserId = (client: SupabaseClient, userId: string) => {
|
||||||
|
return client
|
||||||
|
.from("project")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
project_name,
|
||||||
|
business_id:business!inner (
|
||||||
|
user_id
|
||||||
|
),
|
||||||
|
dataroom_id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq("business.user_id", userId);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getProjectData,
|
getProjectData,
|
||||||
getProjectDataQuery,
|
getProjectDataQuery,
|
||||||
getTopProjects,
|
getTopProjects,
|
||||||
searchProjectsQuery,
|
searchProjectsQuery,
|
||||||
|
getProjectByBusinessId,
|
||||||
|
getProjectByUserId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
|
||||||
|
|
||||||
function getBusinesses(client: SupabaseClient, query: string | null) {
|
|
||||||
return client
|
|
||||||
.from("business")
|
|
||||||
.select("id, business_name, joined_date")
|
|
||||||
.ilike("business_name", `%${query}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjects(client: SupabaseClient, businessIds: string[]) {
|
|
||||||
return client
|
|
||||||
.from("project")
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
id,
|
|
||||||
project_name,
|
|
||||||
business_id,
|
|
||||||
published_time,
|
|
||||||
project_short_description,
|
|
||||||
project_investment_detail (
|
|
||||||
min_investment,
|
|
||||||
total_investment,
|
|
||||||
target_investment
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.in("business_id", businessIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTags(client: SupabaseClient, projectIds: string[]) {
|
|
||||||
return client
|
|
||||||
.from("item_tag")
|
|
||||||
.select("item_id, tag (value)")
|
|
||||||
.in("item_id", projectIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInvestmentCounts(client: SupabaseClient, projectIds: string[]) {
|
|
||||||
return client
|
|
||||||
.from("investment_deal")
|
|
||||||
.select("*", {
|
|
||||||
count: "exact",
|
|
||||||
head: true,
|
|
||||||
})
|
|
||||||
.in("project_id", projectIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInvestorDeal(client: SupabaseClient, userId: string) {
|
|
||||||
return client
|
|
||||||
.from("investment_deal")
|
|
||||||
.select("*")
|
|
||||||
.in("investor_id", [userId])
|
|
||||||
.order("created_time", { ascending: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjectTag(client: SupabaseClient, projectId: number) {
|
|
||||||
return client.from("project_tag").select("tag_id").in("item_id", [projectId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagName(client: SupabaseClient, tagId: number) {
|
|
||||||
return client.from("tag").select("value").in("id", [tagId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getBusinesses,
|
|
||||||
getInvestmentCounts,
|
|
||||||
getProjects,
|
|
||||||
getTags,
|
|
||||||
getInvestorDeal,
|
|
||||||
getProjectTag,
|
|
||||||
getTagName,
|
|
||||||
};
|
|
||||||
5
src/lib/data/tagQuery.ts
Normal file
5
src/lib/data/tagQuery.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const getTagsByProjectIds = (client: SupabaseClient, projectIds: string[]) => {
|
||||||
|
return client.from("project_tag").select(`item_id, ...tag (tag_value:value)`).in("item_id", projectIds);
|
||||||
|
};
|
||||||
@ -20,4 +20,8 @@ async function getUserProfile(client: SupabaseClient, userId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getUserProfile };
|
function getUserRole(client: SupabaseClient, userId: string) {
|
||||||
|
return client.from("user_role").select("role").eq("user_id", userId).single();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getUserProfile, getUserRole };
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export async function updateSession(request: NextRequest) {
|
|||||||
return request.cookies.getAll();
|
return request.cookies.getAll();
|
||||||
},
|
},
|
||||||
setAll(cookiesToSet) {
|
setAll(cookiesToSet) {
|
||||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
|
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
||||||
supabaseResponse = NextResponse.next({
|
supabaseResponse = NextResponse.next({
|
||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
import { createServerClient } from "@supabase/ssr";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
export function createSupabaseClient() {
|
export function createSupabaseClient() {
|
||||||
@ -12,7 +12,9 @@ export function createSupabaseClient() {
|
|||||||
setAll(cookiesToSet) {
|
setAll(cookiesToSet) {
|
||||||
try {
|
try {
|
||||||
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
|
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
console.error("Error setting cookies:", error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,3 +4,30 @@ import { twMerge } from "tailwind-merge"
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sum(list: any[]) {
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.reduce((total, num) => total + num, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sumByKey(list: any[], key: string) {
|
||||||
|
// example usage
|
||||||
|
// const items = [
|
||||||
|
// { amount: 10 },
|
||||||
|
// { amount: 20 },
|
||||||
|
// { amount: 30 },
|
||||||
|
// { amount: 40 }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// const totalAmount = sumByKey(items, 'amount');
|
||||||
|
// console.log(totalAmount); // Output: 100
|
||||||
|
return list.reduce((total, obj) => total + (obj[key] || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPercentage(part: number, total: number): number {
|
||||||
|
if (total === 0) return 0; // Prevent division by zero
|
||||||
|
return Number(((part * 100) / total).toFixed(2));
|
||||||
|
}
|
||||||
Binary file not shown.
27
src/types/schemas/profile.schema.ts
Normal file
27
src/types/schemas/profile.schema.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 500000;
|
||||||
|
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
|
||||||
|
|
||||||
|
const profileAvatarSchema = z
|
||||||
|
.custom<File>(
|
||||||
|
(val) =>
|
||||||
|
val && typeof val === "object" && "size" in val && "type" in val,
|
||||||
|
{
|
||||||
|
message: "Input must be a file.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine((file) => file.size < MAX_FILE_SIZE, {
|
||||||
|
message: "File can't be bigger than 5MB.",
|
||||||
|
})
|
||||||
|
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
||||||
|
message: "File format must be either jpg, jpeg, or png.",
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
export const profileSchema = z.object({
|
||||||
|
username: z.string().min(3).max(50).optional(),
|
||||||
|
full_name: z.string().min(4).max(100).optional(),
|
||||||
|
bio: z.string().min(10).max(1000).optional(),
|
||||||
|
updated_at: z.string().datetime().optional(),
|
||||||
|
avatars: profileAvatarSchema,
|
||||||
|
});
|
||||||
51
tests/test-1-project-apply.spec.ts
Normal file
51
tests/test-1-project-apply.spec.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { test, Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
// Helper function to handle dropdown selection with retry mechanism, automatically selecting the first option
|
||||||
|
const selectFirstOption = async (page: Page, triggerLocator: Locator) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await triggerLocator.hover();
|
||||||
|
await triggerLocator.click({ force: true });
|
||||||
|
|
||||||
|
// Select the first available option
|
||||||
|
const firstOption = page.getByRole("option").first();
|
||||||
|
await firstOption.waitFor({ state: 'visible', timeout: 1000 });
|
||||||
|
await firstOption.click();
|
||||||
|
console.log("First option selected.");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Retrying as the combobox disappeared...");
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.goto("http://127.0.0.1:3000/");
|
||||||
|
await page.getByRole('button', { name: 'Projects' }).hover();
|
||||||
|
await page.getByRole('link', { name: 'Projects Start your new' }).click();
|
||||||
|
|
||||||
|
await page.locator('#projectName').fill('DummyTester');
|
||||||
|
await page.locator('#projectName').fill('DummyTester');
|
||||||
|
|
||||||
|
// Select first option in 'Select a Project type'
|
||||||
|
const projectTypeButton = page.locator('button').filter({ hasText: 'Select a Project type' });
|
||||||
|
await selectFirstOption(page, projectTypeButton);
|
||||||
|
|
||||||
|
await page.locator('#shortDescription').fill('0123456789');
|
||||||
|
await page.getByPlaceholder('https:// ').fill('https://www.google.com/');
|
||||||
|
await page.getByPlaceholder('$ 500').fill('499');
|
||||||
|
await page.getByPlaceholder('$ 1,000,000').fill('99999999');
|
||||||
|
await page.locator('#deadline').fill('2024-11-29T21:19');
|
||||||
|
|
||||||
|
// Log the text content of the second combobox
|
||||||
|
const tag = page.getByRole('combobox').nth(1);
|
||||||
|
const tagText = await tag.textContent();
|
||||||
|
console.log('Tag text:', tagText);
|
||||||
|
|
||||||
|
// Select first option in the tag combobox
|
||||||
|
await selectFirstOption(page, tag);
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
await page.getByRole('button', { name: 'Submit application' }).click();
|
||||||
|
});
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.use({
|
|
||||||
storageState: './storageState.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Test search businesses', async ({ page }) => {
|
|
||||||
await page.goto('http://127.0.0.1:3000/');
|
|
||||||
await page.getByLabel('Main').getByRole('img').click();
|
|
||||||
|
|
||||||
const businessInput = page.getByPlaceholder('Enter business name...');
|
|
||||||
await expect(businessInput).toBeVisible();
|
|
||||||
await businessInput.fill('neon');
|
|
||||||
await businessInput.press('Enter');
|
|
||||||
|
|
||||||
const heading = page.getByRole('heading', { name: 'Neon Solution, A dummy company' });
|
|
||||||
await expect(heading).toBeVisible();
|
|
||||||
await heading.click();
|
|
||||||
|
|
||||||
const fundSection = page.locator('div').filter({ hasText: /^Neon raising fund #1$/ });
|
|
||||||
await expect(fundSection).toBeVisible();
|
|
||||||
await fundSection.click();
|
|
||||||
});
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.use({
|
|
||||||
storageState: './storageState.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Test filter with tags', async ({ page }) => {
|
|
||||||
await page.goto('http://127.0.0.1:3000/');
|
|
||||||
|
|
||||||
// Start Investing
|
|
||||||
await page.getByRole('button', { name: 'Start Investing' }).click();
|
|
||||||
|
|
||||||
// Filter by AI tag
|
|
||||||
await page.locator('button').filter({ hasText: 'Tags' }).click();
|
|
||||||
await page.getByLabel('AI', { exact: true }).click();
|
|
||||||
const aiTag = page.locator('span#tag', { hasText: 'AI' });
|
|
||||||
await expect(aiTag).toBeVisible();
|
|
||||||
|
|
||||||
// Filter by Technology tag
|
|
||||||
await page.locator('button').filter({ hasText: 'AI' }).click();
|
|
||||||
await page.getByLabel('Technology').click();
|
|
||||||
const techTag = page.locator('span#tag', { hasText: 'Technology' });
|
|
||||||
await expect(techTag).toBeVisible();
|
|
||||||
|
|
||||||
// Filter by Consumer Electronics tag
|
|
||||||
await page.locator('button').filter({ hasText: 'Technology' }).click();
|
|
||||||
await page.getByLabel('Consumer Electronics').click();
|
|
||||||
const consumerElectronicsTag = page.locator('span#tag', { hasText: 'Consumer Electronics' });
|
|
||||||
await expect(consumerElectronicsTag).toBeVisible();
|
|
||||||
|
|
||||||
// Filter by Software tag
|
|
||||||
await page.locator('button').filter({ hasText: 'Consumer Electronics' }).click();
|
|
||||||
await page.getByLabel('Software').click();
|
|
||||||
const softwareTag = page.locator('span#tag', { hasText: 'Software' });
|
|
||||||
await expect(softwareTag).toBeVisible();
|
|
||||||
|
|
||||||
// Filter by Internet tag
|
|
||||||
await page.locator('button').filter({ hasText: 'Software' }).click();
|
|
||||||
await page.getByLabel('Internet').click();
|
|
||||||
const internetTag = page.locator('span#tag', { hasText: 'Internet' });
|
|
||||||
await expect(internetTag).toBeVisible();
|
|
||||||
});
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test.use({
|
|
||||||
storageState: './storageState.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Test dashboard visibility', async ({ page }) => {
|
|
||||||
await page.goto('http://127.0.0.1:3000/dashboard');
|
|
||||||
|
|
||||||
const dashboardHeading = page.locator('h2', { hasText: 'Dashboard' });
|
|
||||||
await expect(dashboardHeading).toBeVisible();
|
|
||||||
|
|
||||||
const profileViewHeading = page.locator('h3', { hasText: 'Profile Views' });
|
|
||||||
await expect(profileViewHeading).toBeVisible();
|
|
||||||
|
|
||||||
const totalFollowerHeading = page.locator('h3', { hasText: 'Total Followers' });
|
|
||||||
await expect(totalFollowerHeading).toBeVisible();
|
|
||||||
|
|
||||||
const fundsRaisedHeading = page.locator('h3', { hasText: 'Total Funds Raised' });
|
|
||||||
await expect(fundsRaisedHeading).toBeVisible();
|
|
||||||
|
|
||||||
const overviewHeading = page.locator('h3', { hasText: 'Overview' });
|
|
||||||
await expect(overviewHeading).toBeVisible();
|
|
||||||
|
|
||||||
const recentFundHeading = page.locator('h3', { hasText: 'Recent Funds' });
|
|
||||||
await expect(recentFundHeading).toBeVisible();
|
|
||||||
});
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { test, expect, Page } from '@playwright/test';
|
|
||||||
|
|
||||||
test.use({
|
|
||||||
storageState: './storageState.json',
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Investment process test', async ({ page }) => {
|
|
||||||
await page.goto('http://127.0.0.1:3000/');
|
|
||||||
|
|
||||||
// Navigate to the investment page
|
|
||||||
// await page.getByRole('link', { name: 'Card image NVDA Founded in' }).click();
|
|
||||||
await page.click('a[href="/invest"]');
|
|
||||||
await page.getByRole('button', { name: 'Invest in NVIDIA' }).click();
|
|
||||||
|
|
||||||
// Fill investment amount
|
|
||||||
await fillInvestmentAmount(page, '10000');
|
|
||||||
|
|
||||||
// Fill card information
|
|
||||||
await fillCardInformation(page, {
|
|
||||||
name: 'Dummy',
|
|
||||||
city: 'Bangkok',
|
|
||||||
cardNumber: '4111 1111 1111 1111',
|
|
||||||
expirationMonth: 'August',
|
|
||||||
expirationYear: '2032',
|
|
||||||
cvc: '111',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Accept terms
|
|
||||||
await acceptTerms(page, [
|
|
||||||
'Minimum Investment',
|
|
||||||
'Investment Horizon',
|
|
||||||
'Fees',
|
|
||||||
'Returns',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click Invest button and confirm
|
|
||||||
await page.getByRole('button', { name: 'Invest' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
|
||||||
|
|
||||||
// Ensure error message is displayed when not all terms are accepted
|
|
||||||
await ensureErrorMessageDisplayed(page, 'Please accept all terms');
|
|
||||||
|
|
||||||
// Close the error dialog
|
|
||||||
await closeErrorDialog(page);
|
|
||||||
|
|
||||||
// Accept remaining terms
|
|
||||||
await acceptTerms(page, [
|
|
||||||
'Risk Disclosure',
|
|
||||||
'Withdrawal Policy',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click Invest button and confirm again
|
|
||||||
await page.getByRole('button', { name: 'Invest' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
|
||||||
|
|
||||||
// Ensure that success toast is displayed when investment is successful
|
|
||||||
await expect(
|
|
||||||
page.locator('div[role="status"][aria-live="polite"]').filter({ hasText: /^You successfully invested!$/ })
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
async function fillInvestmentAmount(page: Page, amount: string): Promise<void> {
|
|
||||||
await page.getByPlaceholder('min $').click();
|
|
||||||
await page.getByPlaceholder('min $').fill(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CardInfo {
|
|
||||||
name: string;
|
|
||||||
city: string;
|
|
||||||
cardNumber: string;
|
|
||||||
expirationMonth: string;
|
|
||||||
expirationYear: string;
|
|
||||||
cvc: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillCardInformation(
|
|
||||||
page: Page,
|
|
||||||
{ name, city, cardNumber, expirationMonth, expirationYear, cvc }: CardInfo
|
|
||||||
): Promise<void> {
|
|
||||||
await page.getByPlaceholder('First Last').click();
|
|
||||||
await page.getByPlaceholder('First Last').fill(name);
|
|
||||||
await page.getByLabel('City').click();
|
|
||||||
await page.getByLabel('City').fill(city);
|
|
||||||
await page.getByLabel('Card number').click();
|
|
||||||
await page.getByLabel('Card number').fill(cardNumber);
|
|
||||||
await page.getByLabel('Month').click();
|
|
||||||
await page.getByText(expirationMonth).click();
|
|
||||||
await page.getByLabel('Year').click();
|
|
||||||
await page.getByLabel(expirationYear).click();
|
|
||||||
await page.getByPlaceholder('CVC').click();
|
|
||||||
await page.getByPlaceholder('CVC').fill(cvc);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function acceptTerms(page: Page, terms: string[]): Promise<void> {
|
|
||||||
for (const term of terms) {
|
|
||||||
await page.getByRole('row', { name: new RegExp(term) }).getByRole('checkbox').check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureErrorMessageDisplayed(page: Page, message: string): Promise<void> {
|
|
||||||
await expect(page.getByText(message)).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeErrorDialog(page: Page): Promise<void> {
|
|
||||||
await page.getByRole('button', { name: 'Close' }).first().click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
107
tests/test-Investment-process .spec.ts
Normal file
107
tests/test-Investment-process .spec.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// import { test, expect, Page } from '@playwright/test';
|
||||||
|
//
|
||||||
|
// test.use({
|
||||||
|
// storageState: './storageState.json',
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test('Investment process test', async ({ page }) => {
|
||||||
|
// await page.goto('http://127.0.0.1:3000/');
|
||||||
|
//
|
||||||
|
// // Navigate to the investment page
|
||||||
|
// // await page.getByRole('link', { name: 'Card image NVDA Founded in' }).click();
|
||||||
|
// await page.click('a[href="/invest"]');
|
||||||
|
// await page.getByRole('button', { name: 'Invest in NVIDIA' }).click();
|
||||||
|
//
|
||||||
|
// // Fill investment amount
|
||||||
|
// await fillInvestmentAmount(page, '10000');
|
||||||
|
//
|
||||||
|
// // Fill card information
|
||||||
|
// await fillCardInformation(page, {
|
||||||
|
// name: 'Dummy',
|
||||||
|
// city: 'Bangkok',
|
||||||
|
// cardNumber: '4111 1111 1111 1111',
|
||||||
|
// expirationMonth: 'August',
|
||||||
|
// expirationYear: '2032',
|
||||||
|
// cvc: '111',
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // Accept terms
|
||||||
|
// await acceptTerms(page, [
|
||||||
|
// 'Minimum Investment',
|
||||||
|
// 'Investment Horizon',
|
||||||
|
// 'Fees',
|
||||||
|
// 'Returns',
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// // Click Invest button and confirm
|
||||||
|
// await page.getByRole('button', { name: 'Invest' }).click();
|
||||||
|
// await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
//
|
||||||
|
// // Ensure error message is displayed when not all terms are accepted
|
||||||
|
// await ensureErrorMessageDisplayed(page, 'Please accept all terms');
|
||||||
|
//
|
||||||
|
// // Close the error dialog
|
||||||
|
// await closeErrorDialog(page);
|
||||||
|
//
|
||||||
|
// // Accept remaining terms
|
||||||
|
// await acceptTerms(page, [
|
||||||
|
// 'Risk Disclosure',
|
||||||
|
// 'Withdrawal Policy',
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// // Click Invest button and confirm again
|
||||||
|
// await page.getByRole('button', { name: 'Invest' }).click();
|
||||||
|
// await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
//
|
||||||
|
// // Ensure that success toast is displayed when investment is successful
|
||||||
|
// await expect(
|
||||||
|
// page.locator('div[role="status"][aria-live="polite"]').filter({ hasText: /^You successfully invested!$/ })
|
||||||
|
// ).toBeVisible();
|
||||||
|
//
|
||||||
|
// // Helper functions
|
||||||
|
// async function fillInvestmentAmount(page: Page, amount: string): Promise<void> {
|
||||||
|
// await page.getByPlaceholder('min $').click();
|
||||||
|
// await page.getByPlaceholder('min $').fill(amount);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// interface CardInfo {
|
||||||
|
// name: string;
|
||||||
|
// city: string;
|
||||||
|
// cardNumber: string;
|
||||||
|
// expirationMonth: string;
|
||||||
|
// expirationYear: string;
|
||||||
|
// cvc: string;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async function fillCardInformation(
|
||||||
|
// page: Page,
|
||||||
|
// { name, city, cardNumber, expirationMonth, expirationYear, cvc }: CardInfo
|
||||||
|
// ): Promise<void> {
|
||||||
|
// await page.getByPlaceholder('First Last').click();
|
||||||
|
// await page.getByPlaceholder('First Last').fill(name);
|
||||||
|
// await page.getByLabel('City').click();
|
||||||
|
// await page.getByLabel('City').fill(city);
|
||||||
|
// await page.getByLabel('Card number').click();
|
||||||
|
// await page.getByLabel('Card number').fill(cardNumber);
|
||||||
|
// await page.getByLabel('Month').click();
|
||||||
|
// await page.getByText(expirationMonth).click();
|
||||||
|
// await page.getByLabel('Year').click();
|
||||||
|
// await page.getByLabel(expirationYear).click();
|
||||||
|
// await page.getByPlaceholder('CVC').click();
|
||||||
|
// await page.getByPlaceholder('CVC').fill(cvc);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async function acceptTerms(page: Page, terms: string[]): Promise<void> {
|
||||||
|
// for (const term of terms) {
|
||||||
|
// await page.getByRole('row', { name: new RegExp(term) }).getByRole('checkbox').check();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async function ensureErrorMessageDisplayed(page: Page, message: string): Promise<void> {
|
||||||
|
// await expect(page.getByText(message)).toBeVisible();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async function closeErrorDialog(page: Page): Promise<void> {
|
||||||
|
// await page.getByRole('button', { name: 'Close' }).first().click();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
23
tests/test-businesses.spec.ts
Normal file
23
tests/test-businesses.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// import { test, expect } from "@playwright/test";
|
||||||
|
//
|
||||||
|
// test.use({
|
||||||
|
// storageState: "./storageState.json",
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test("Test search businesses", async ({ page }) => {
|
||||||
|
// await page.goto("http://127.0.0.1:3000/");
|
||||||
|
// await page.getByLabel("Main").getByRole("img").click();
|
||||||
|
//
|
||||||
|
// const businessInput = page.getByPlaceholder("Enter business name...");
|
||||||
|
// await expect(businessInput).toBeVisible();
|
||||||
|
// await businessInput.fill("Project Blackwell");
|
||||||
|
// await businessInput.press("Enter");
|
||||||
|
//
|
||||||
|
// const heading = page.getByRole("heading", { name: "Project Blackwell" });
|
||||||
|
// await expect(heading).toBeVisible();
|
||||||
|
// await heading.click();
|
||||||
|
//
|
||||||
|
// const fundSection = page.locator("div").filter({ hasText: /^Project Blackwell$/ });
|
||||||
|
// await expect(fundSection).toBeVisible();
|
||||||
|
// await fundSection.click();
|
||||||
|
// });
|
||||||
26
tests/test-dashboard-visibility.spec.ts
Normal file
26
tests/test-dashboard-visibility.spec.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// import { test, expect } from '@playwright/test';
|
||||||
|
// test.use({
|
||||||
|
// storageState: './storageState.json'
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test('Test dashboard visibility', async ({ page }) => {
|
||||||
|
// await page.goto('http://127.0.0.1:3000/dashboard');
|
||||||
|
//
|
||||||
|
// const dashboardHeading = page.locator('h2', { hasText: 'Dashboard' });
|
||||||
|
// await expect(dashboardHeading).toBeVisible();
|
||||||
|
//
|
||||||
|
// const profileViewHeading = page.locator('h3', { hasText: 'Profile Views' });
|
||||||
|
// await expect(profileViewHeading).toBeVisible();
|
||||||
|
//
|
||||||
|
// const totalFollowerHeading = page.locator('h3', { hasText: 'Total Followers' });
|
||||||
|
// await expect(totalFollowerHeading).toBeVisible();
|
||||||
|
//
|
||||||
|
// const fundsRaisedHeading = page.locator('h3', { hasText: 'Total Funds Raised' });
|
||||||
|
// await expect(fundsRaisedHeading).toBeVisible();
|
||||||
|
//
|
||||||
|
// const overviewHeading = page.locator('h3', { hasText: 'Overview' });
|
||||||
|
// await expect(overviewHeading).toBeVisible();
|
||||||
|
//
|
||||||
|
// const recentFundHeading = page.locator('h3', { hasText: 'Recent Funds' });
|
||||||
|
// await expect(recentFundHeading).toBeVisible();
|
||||||
|
// });
|
||||||
42
tests/test-filter-with-tags.spec.ts
Normal file
42
tests/test-filter-with-tags.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// import { test, expect } from '@playwright/test';
|
||||||
|
//
|
||||||
|
// test.use({
|
||||||
|
// storageState: './storageState.json'
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test('Test filter with tags', async ({ page }) => {
|
||||||
|
// await page.goto('http://127.0.0.1:3000/');
|
||||||
|
//
|
||||||
|
// // Start Investing
|
||||||
|
// await page.getByRole('button', { name: 'Start Investing' }).click();
|
||||||
|
//
|
||||||
|
// // Filter by AI tag
|
||||||
|
// await page.locator('button').filter({ hasText: 'Tags' }).click();
|
||||||
|
// await page.getByLabel('AI', { exact: true }).click();
|
||||||
|
// const aiTag = page.locator('span#tag', { hasText: 'AI' });
|
||||||
|
// await expect(aiTag).toBeVisible();
|
||||||
|
//
|
||||||
|
// // Filter by Technology tag
|
||||||
|
// await page.locator('button').filter({ hasText: 'AI' }).click();
|
||||||
|
// await page.getByLabel('Technology').click();
|
||||||
|
// const techTag = page.locator('span#tag', { hasText: 'Technology' });
|
||||||
|
// await expect(techTag).toBeVisible();
|
||||||
|
//
|
||||||
|
// // Filter by Consumer Electronics tag
|
||||||
|
// await page.locator('button').filter({ hasText: 'Technology' }).click();
|
||||||
|
// await page.getByLabel('Consumer Electronics').click();
|
||||||
|
// const consumerElectronicsTag = page.locator('span#tag', { hasText: 'Consumer Electronics' });
|
||||||
|
// await expect(consumerElectronicsTag).toBeVisible();
|
||||||
|
//
|
||||||
|
// // Filter by Software tag
|
||||||
|
// await page.locator('button').filter({ hasText: 'Consumer Electronics' }).click();
|
||||||
|
// await page.getByLabel('Software').click();
|
||||||
|
// const softwareTag = page.locator('span#tag', { hasText: 'Software' });
|
||||||
|
// await expect(softwareTag).toBeVisible();
|
||||||
|
//
|
||||||
|
// // Filter by Internet tag
|
||||||
|
// await page.locator('button').filter({ hasText: 'Software' }).click();
|
||||||
|
// await page.getByLabel('Internet').click();
|
||||||
|
// const internetTag = page.locator('span#tag', { hasText: 'Internet' });
|
||||||
|
// await expect(internetTag).toBeVisible();
|
||||||
|
// });
|
||||||
@ -4,6 +4,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user