mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 13:34:06 +01:00
Merge pull request #76 from Sosokker/back-end
Add admin page + data room page + enable profile edit + Fix all eslint error + povide ci/cd
This commit is contained in:
commit
f4d24ab214
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
|
||||
# on:
|
||||
# push:
|
||||
# branches: [ main, master ]
|
||||
# pull_request:
|
||||
# branches: [ main, master ]
|
||||
# jobs:
|
||||
# test:
|
||||
# timeout-minutes: 60
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: lts/*
|
||||
# - name: Install dependencies
|
||||
# run: npm ci
|
||||
# - name: Install Playwright Browsers
|
||||
# run: npx playwright install --with-deps
|
||||
# - name: Run Playwright tests
|
||||
# run: npx playwright test
|
||||
# - uses: actions/upload-artifact@v4
|
||||
# if: ${{ !cancelled() }}
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: playwright-report/
|
||||
# retention-days: 30
|
||||
name: Playwright Tests
|
||||
|
||||
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: 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
|
||||
|
||||
## About
|
||||
|
||||
2518
package-lock.json
generated
2518
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",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
@ -30,7 +32,7 @@
|
||||
"@stripe/stripe-js": "^4.7.0",
|
||||
"@supabase-cache-helpers/postgrest-react-query": "^1.10.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-devtools": "^5.59.0",
|
||||
"b2d-ventures": "file:",
|
||||
@ -45,35 +47,43 @@
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-day-picker": "^9",
|
||||
"react-dom": "^18",
|
||||
"react-file-icon": "^1.5.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-lottie": "^1.2.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"recharts": "^2.12.7",
|
||||
"stripe": "^17.1.0",
|
||||
"sweetalert2": "^11.14.3",
|
||||
"sweetalert2": "^11.6.13",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/next": "^8.0.7",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-fade-in": "^2.0.2",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-lottie": "^1.2.10",
|
||||
"@types/react-select-country-list": "^2.2.3",
|
||||
"eslint": "^8",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"supabase": "^1.200.3",
|
||||
"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";
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ShareIcon, StarIcon } from "lucide-react";
|
||||
|
||||
@ -13,24 +13,42 @@ import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||
import FollowShareButtons from "./followShareButton";
|
||||
|
||||
import { getProjectData } from "@/lib/data/projectQuery";
|
||||
import { getDealList } from "@/app/api/dealApi";
|
||||
import { sumByKey, toPercentage } from "@/lib/utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ProjectDealPage({ params }: { params: { id: number } }) {
|
||||
const supabase = createSupabaseClient();
|
||||
|
||||
const { data: projectData, error: projectDataError } = await getProjectData(supabase, params.id);
|
||||
|
||||
const carouselData = [
|
||||
{ src: "/boiler1.jpg", alt: "Boiler 1" },
|
||||
{ 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 (!projectData) {
|
||||
redirect("/deals");
|
||||
}
|
||||
|
||||
if (projectDataError) {
|
||||
return <div>Error</div>;
|
||||
return (
|
||||
<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 (
|
||||
<div className="container max-w-screen-xl my-5">
|
||||
<div className="flex flex-col gap-y-10">
|
||||
@ -82,24 +100,33 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
||||
<div id="stats" className="flex flex-col w-full mt-4 pl-12">
|
||||
<div className="pl-5">
|
||||
<span>
|
||||
<h1 className="font-semibold text-xl md:text-4xl mt-8">${projectData?.total_investment}</h1>
|
||||
<p className="text-sm md:text-lg"> 5% raised of \$5M max goal</p>
|
||||
<h1 className="font-semibold text-xl md:text-4xl mt-8">${totalDealAmount}</h1>
|
||||
<p className="text-sm md:text-lg">
|
||||
{toPercentage(totalDealAmount, projectData?.target_investment)}% raised of $
|
||||
{projectData?.target_investment} max goal
|
||||
</p>
|
||||
<Progress
|
||||
value={projectData?.total_investment / projectData?.target_investment}
|
||||
value={toPercentage(totalDealAmount, projectData?.target_investment)}
|
||||
className="w-[60%] h-3 mt-3"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<h1 className="font-semibold text-4xl md:mt-8">
|
||||
<p className="text-xl md:text-4xl">{projectData?.total_investment}</p>
|
||||
<p className="text-xl md:text-4xl">{dealList.length}</p>
|
||||
</h1>
|
||||
<p className="text-sm md:text-lg"> Investors</p>
|
||||
<p className="text-sm md:text-lg">Investors</p>
|
||||
</span>
|
||||
<Separator decorative className="mt-3 w-3/4 ml-5" />
|
||||
<span>
|
||||
<h1 className="font-semibold text-xl md:text-4xl mt-8 ml-5"></h1>
|
||||
<p className="text-xl md:text-4xl">1 hours</p>
|
||||
<p> Left to invest</p>
|
||||
{projectData?.investment_deadline ? (
|
||||
<>
|
||||
<p className="text-xl md:text-4xl">{Math.floor(hourLeft)} hours</p>
|
||||
<p>Left to invest</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xl md:text-4xl">No deadline</p>
|
||||
)}
|
||||
</span>
|
||||
<Button className="mt-5 w-3/4 h-12">
|
||||
<Link href={`/invest/${params.id}`}>Invest in {projectData?.project_name}</Link>
|
||||
@ -121,8 +148,8 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
||||
<Tabs.Content value="pitch">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle></CardTitle>
|
||||
<CardDescription></CardDescription>
|
||||
<CardTitle>{projectData.project_name}</CardTitle>
|
||||
<CardDescription />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-sm max-w-none ">
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
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 { Separator } from "@/components/ui/separator";
|
||||
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 { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||
import { searchProjectsQuery, FilterParams, FilterProjectQueryParams } from "@/lib/data/projectQuery";
|
||||
@ -13,7 +13,29 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
const ProjectSection = ({ filteredProjects }) => {
|
||||
interface Project {
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
published_time: string;
|
||||
project_short_description: string;
|
||||
card_image_url: string;
|
||||
project_status: {
|
||||
value: string;
|
||||
};
|
||||
min_investment: number;
|
||||
total_investment: number;
|
||||
target_investment: number;
|
||||
investment_deadline: string;
|
||||
tags: {
|
||||
tag_name: string;
|
||||
}[];
|
||||
business_type: {
|
||||
value: string;
|
||||
};
|
||||
business_location: string;
|
||||
}
|
||||
|
||||
const ProjectSection = ({ filteredProjects }: { filteredProjects: Project[] }) => {
|
||||
interface Tags {
|
||||
tag_name: string;
|
||||
}
|
||||
@ -117,7 +139,7 @@ export default function Deals() {
|
||||
const [sortByTimeFilter, setSortByTimeFilter] = useState("all");
|
||||
const [businessTypeFilter, setBusinessTypeFilter] = useState("all");
|
||||
const [tagFilter, setTagFilter] = useState([]);
|
||||
const [projectStatusFilter, setprojectStatusFilter] = useState("all");
|
||||
const [projectStatusFilter, setProjectStatusFilter] = useState("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(4);
|
||||
|
||||
@ -130,16 +152,15 @@ export default function Deals() {
|
||||
};
|
||||
|
||||
const filterProjectQueryParams: FilterProjectQueryParams = {
|
||||
searchTerm,
|
||||
tagsFilter: tagFilter,
|
||||
projectStatusFilter,
|
||||
businessTypeFilter,
|
||||
sortByTimeFilter,
|
||||
searchTerm: "",
|
||||
tagsFilter: [],
|
||||
projectStatusFilter: "all",
|
||||
businessTypeFilter: "all",
|
||||
sortByTimeFilter: "all",
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
const { data: tags, isLoading: isLoadingTags, error: tagsLoadingError } = useQuery(getAllTagsQuery(supabase));
|
||||
const {
|
||||
data: projectStatus,
|
||||
isLoading: isLoadingFunded,
|
||||
@ -160,11 +181,20 @@ export default function Deals() {
|
||||
const clearAll = () => {
|
||||
setSearchTerm("");
|
||||
setTagFilter([]);
|
||||
setprojectStatusFilter("all");
|
||||
setProjectStatusFilter("all");
|
||||
setBusinessTypeFilter("all");
|
||||
setSortByTimeFilter("all");
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (value: number) => {
|
||||
setPageSize(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(searchTermVisual);
|
||||
}, [searchTermVisual]);
|
||||
|
||||
return (
|
||||
<div className="container max-w-screen-xl mx-auto px-4">
|
||||
<div className="h-auto mt-10">
|
||||
@ -175,8 +205,7 @@ export default function Deals() {
|
||||
All companies are <u>vetted & pass due diligence.</u>
|
||||
</p>
|
||||
|
||||
{/* {JSON.stringify(projects, null, 4)} */}
|
||||
|
||||
{/* Search Input and Filters */}
|
||||
<div className="flex mt-10 gap-3">
|
||||
<Input
|
||||
type="text"
|
||||
@ -205,7 +234,7 @@ export default function Deals() {
|
||||
</Select>
|
||||
|
||||
{/* Business Type Filter */}
|
||||
<Select onValueChange={(value) => setBusinessTypeFilter}>
|
||||
<Select onValueChange={(value) => setBusinessTypeFilter(value)}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<UsersIcon className="ml-2" />
|
||||
<SelectValue placeholder="Business Type" />
|
||||
@ -234,7 +263,7 @@ export default function Deals() {
|
||||
</Select>
|
||||
|
||||
{/* Project Status Filter */}
|
||||
<Select onValueChange={(key) => setprojectStatusFilter(key)}>
|
||||
<Select onValueChange={(key) => setProjectStatusFilter(key)}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<UserIcon className="ml-2" />
|
||||
<SelectValue placeholder="Project Status" />
|
||||
@ -262,11 +291,54 @@ export default function Deals() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Active Filters */}
|
||||
<ShowFilter filterParams={filterParams} clearAll={clearAll} />
|
||||
<Separator className="mt-10" />
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 {
|
||||
Dialog,
|
||||
@ -14,9 +14,9 @@ import {
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useSession from "@/lib/supabase/useSession";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const CheckoutPage = ({
|
||||
amount,
|
||||
@ -35,11 +35,8 @@ const CheckoutPage = ({
|
||||
const [clientSecret, setClientSecret] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); // New success dialog state
|
||||
const isAcceptTerm = isAcceptTermAndService();
|
||||
const router = useRouter();
|
||||
|
||||
const { session } = useSession();
|
||||
const user = session?.user;
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/create-payment-intent", {
|
||||
@ -72,7 +69,7 @@ const CheckoutPage = ({
|
||||
await stripe
|
||||
.confirmCardPayment(clientSecret, {
|
||||
payment_method: {
|
||||
card: elements.getElement(CardElement)!,
|
||||
card: elements.getElement(CardNumberElement)!,
|
||||
},
|
||||
})
|
||||
.then(async (result) => {
|
||||
@ -90,12 +87,15 @@ const CheckoutPage = ({
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
toast.error("Unexpected error with server");
|
||||
console.error("Supabase Insert Error:", error.message);
|
||||
} else {
|
||||
console.log("Insert successful:", data);
|
||||
router.push(`http://www.localhost:3000/payment-success?amount=${amount}`);
|
||||
toast.success("Invest successfully");
|
||||
setIsSuccessDialogOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Unexpected error with server");
|
||||
console.error("Unexpected error during Supabase insert:", err);
|
||||
}
|
||||
}
|
||||
@ -108,7 +108,8 @@ const CheckoutPage = ({
|
||||
<div className="flex items-center justify-center">
|
||||
<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"
|
||||
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)]">
|
||||
Loading...
|
||||
</span>
|
||||
@ -119,7 +120,70 @@ const CheckoutPage = ({
|
||||
|
||||
return (
|
||||
<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>}
|
||||
|
||||
@ -130,7 +194,8 @@ const CheckoutPage = ({
|
||||
type="button"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
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..."}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
@ -154,6 +219,23 @@ const CheckoutPage = ({
|
||||
{errorMessage && <p className="text-red-500 mt-2 text-lg font-bold">{errorMessage}</p>}
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@ -15,7 +14,6 @@ import { loadStripe } from "@stripe/stripe-js";
|
||||
|
||||
import { getProjectDataQuery } from "@/lib/data/projectQuery";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import useSession from "@/lib/supabase/useSession";
|
||||
|
||||
if (process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY === undefined) {
|
||||
throw new Error("NEXT_PUBLIC_STRIPE_PUBLIC_KEY is not defined");
|
||||
@ -72,7 +70,11 @@ export default function InvestPage() {
|
||||
fetchInvestorData();
|
||||
}, [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 updatedCheckedTerms = [...checkedTerms];
|
||||
@ -89,57 +91,69 @@ export default function InvestPage() {
|
||||
|
||||
return (
|
||||
<div className="mx-10 md:mx-40 my-10">
|
||||
<h1 className="text-2xl md:text-4xl font-bold">Invest on ${projectData?.project_name}</h1>
|
||||
<Separator className="my-4" />
|
||||
<div></div>
|
||||
<div>
|
||||
<div className="w-1/2 space-y-2">
|
||||
<h2 className="text:base md:text-2xl">Investment Amount</h2>
|
||||
<Input
|
||||
className="w-52"
|
||||
type="number"
|
||||
placeholder="min $10"
|
||||
min={10}
|
||||
onChangeCapture={(e) => setInvestAmount(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
{isLoadingProject ? (
|
||||
<p>Loading project details...</p>
|
||||
) : projectError ? (
|
||||
<p>Error loading project data. Please try again later.</p>
|
||||
) : projectData ? (
|
||||
<>
|
||||
<h1 className="text-2xl md:text-4xl font-bold">Invest in {projectData.project_name}</h1>
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className=" md:w-2/3 space-y-2">
|
||||
<h2 className="text-2xl">Terms and Services</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Select</TableHead>
|
||||
<TableHead>Term</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{term_data.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<input type="checkbox" checked={checkedTerms[index]} onChange={() => handleCheckboxChange(index)} />
|
||||
</TableCell>
|
||||
<TableCell>{item.term}</TableCell>
|
||||
<TableCell>{item.description}</TableCell>
|
||||
{/* Investment Amount Section */}
|
||||
<div className="w-1/2 space-y-2">
|
||||
<h2 className="text:base md:text-2xl">Investment Amount</h2>
|
||||
<Input
|
||||
className="w-52"
|
||||
type="number"
|
||||
placeholder="min $10"
|
||||
min={10}
|
||||
onChangeCapture={(e) => setInvestAmount(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Terms and Services Section */}
|
||||
<div className="md:w-2/3 space-y-2">
|
||||
<h2 className="text-2xl">Terms and Services</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Select</TableHead>
|
||||
<TableHead>Term</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{term_data.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
<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">
|
||||
<h2 className="text:base md:text-2xl">Payment Information</h2>
|
||||
<div>
|
||||
{/* Payment Information Section */}
|
||||
<div className="w-full space-y-2">
|
||||
<h2 className="text:base md:text-2xl">Payment Information</h2>
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
mode: "payment",
|
||||
amount: convertToSubcurrency(investAmount),
|
||||
currency: "usd",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<CheckoutPage
|
||||
amount={investAmount}
|
||||
isAcceptTermAndService={isAcceptTermAndService}
|
||||
@ -148,8 +162,10 @@ export default function InvestPage() {
|
||||
/>
|
||||
</Elements>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>No project data found.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,36 +5,34 @@ import { BellIcon } from "lucide-react";
|
||||
|
||||
export default function Notification() {
|
||||
const sampleNotifications = [
|
||||
{ message: "New message from John Doe", time: "5 minutes ago" },
|
||||
{ message: "Your order has been shipped", time: "2 hours ago" },
|
||||
{ message: "Meeting reminder: Team sync at 3 PM", time: "1 day ago" },
|
||||
{ id: 1, message: "New message from John Doe", time: "5 minutes ago" },
|
||||
{ id: 2, message: "Your order has been shipped", time: "2 hours ago" },
|
||||
{ id: 3, message: "Meeting reminder: Team sync at 3 PM", time: "1 day ago" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ml-24 md:ml-56 mt-16 ">
|
||||
<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 */}
|
||||
<Card className=" border-slate-800 w-3/4 p-6">
|
||||
<Card className="border-slate-800 w-3/4 p-6">
|
||||
<CardContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{sampleNotifications.map((notification, _) => (
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
{sampleNotifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="flex items-center justify-between p-4 border-b border-gray-200"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<BellIcon className="w-5 h-5 text-blue-500 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium ">
|
||||
{notification.message}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{notification.time}
|
||||
</p>
|
||||
<p className="text-sm font-medium">{notification.message}</p>
|
||||
<p className="text-xs text-gray-500">{notification.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-sm text-blue-500 hover:text-blue-600">
|
||||
Mark as read
|
||||
</button>
|
||||
<button className="text-sm text-blue-500 hover:text-blue-600">Mark as read</button>
|
||||
</div>
|
||||
))}
|
||||
</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 { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||
import { getUserProfile } from "@/lib/data/userQuery";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { format } from "date-fns";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface Profile extends Tables<"Profiles"> {}
|
||||
import Link from "next/link";
|
||||
import { getUserRole } from "@/lib/data/userQuery";
|
||||
|
||||
export default async function ProfilePage({ params }: { params: { uid: string } }) {
|
||||
const supabase = createSupabaseClient();
|
||||
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);
|
||||
|
||||
@ -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) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
@ -33,11 +44,13 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
||||
return (
|
||||
<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-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">
|
||||
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Edit Profile
|
||||
</button>
|
||||
{user && user.id === uid && (
|
||||
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
<Link href={`/profile/${uid}/edit`}>Edit Profile</Link>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-2">
|
||||
@ -64,13 +77,19 @@ export default async function ProfilePage({ params }: { params: { uid: string }
|
||||
</a>
|
||||
</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>
|
||||
{/* Lower */}
|
||||
<div>
|
||||
<div className="mt-6">
|
||||
<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>
|
||||
</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 { getCurrentUserID } from "./userApi";
|
||||
|
||||
export type Deal = {
|
||||
deal_amount: number;
|
||||
@ -7,38 +6,133 @@ export type Deal = {
|
||||
investor_id: string;
|
||||
};
|
||||
|
||||
export async function getDealList() {
|
||||
export async function getDealList(userId: string | undefined) {
|
||||
if (!userId) {
|
||||
// console.error("No deal list of this user was found");
|
||||
return []; // Exit on error
|
||||
}
|
||||
|
||||
const supabase = createSupabaseClient();
|
||||
const { data: dealData, error } = await supabase
|
||||
.from('business')
|
||||
.select(`
|
||||
id,
|
||||
// get id of investors who invest in the business
|
||||
const { data: dealData, error: dealError } = await supabase
|
||||
.from("business")
|
||||
.select(
|
||||
`
|
||||
project (
|
||||
id,
|
||||
investment_deal (
|
||||
deal_amount,
|
||||
created_time,
|
||||
investor_id
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('user_id', await getCurrentUserID())
|
||||
`
|
||||
)
|
||||
.eq("user_id", userId)
|
||||
.single();
|
||||
|
||||
if (error || !dealData) {
|
||||
alert(JSON.stringify(error));
|
||||
console.error('Error fetching deal list:', error);
|
||||
} else {
|
||||
const dealList = dealData.project[0].investment_deal;
|
||||
if (dealError) {
|
||||
// alert(JSON.stringify(dealError));
|
||||
console.error("Error fetching deal list:", dealError);
|
||||
return []; // Exit on error
|
||||
}
|
||||
|
||||
if (!dealList.length) {
|
||||
alert("No data available");
|
||||
return; // Exit early if there's no data
|
||||
}
|
||||
if (!dealData || !dealData.project.length) {
|
||||
alert("No project available");
|
||||
return []; // Exit 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);
|
||||
}
|
||||
};
|
||||
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 { LoginButton } from "@/components/auth/loginButton";
|
||||
@ -9,7 +7,8 @@ export default function Login() {
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
||||
style={{ backgroundImage: "url(/login.png)" }}>
|
||||
style={{ backgroundImage: "url(/login.png)" }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className="items-center">
|
||||
<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 { SignupButton } from "@/components/auth/signupButton";
|
||||
@ -9,7 +7,8 @@ export default function Signup() {
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center min-h-screen flex items-center justify-center"
|
||||
style={{ backgroundImage: "url(/signup.png)" }}>
|
||||
style={{ backgroundImage: "url(/signup.png)" }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className="items-center">
|
||||
<CardTitle className="text-2xl font-bold">Join Our Community</CardTitle>
|
||||
|
||||
@ -49,7 +49,7 @@ export default function ApplyBusiness() {
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
const { error } = await supabase
|
||||
.from("business_application")
|
||||
.insert([
|
||||
{
|
||||
@ -60,20 +60,18 @@ export default function ApplyBusiness() {
|
||||
is_for_sale: recvData["isForSale"],
|
||||
is_generating_revenue: recvData["isGenerating"],
|
||||
is_in_us: recvData["isInUS"],
|
||||
pitch_deck_url:
|
||||
pitchType === "string" ? recvData["businessPitchDeck"] : "",
|
||||
pitch_deck_url: pitchType === "string" ? recvData["businessPitchDeck"] : "",
|
||||
money_raised_to_date: recvData["totalRaised"],
|
||||
community_size: recvData["communitySize"],
|
||||
},
|
||||
])
|
||||
.select();
|
||||
setSucess(true);
|
||||
// console.table(data);
|
||||
|
||||
Swal.fire({
|
||||
icon: error == null ? "success" : "error",
|
||||
title: error == null ? "success" : "Error: " + error.code,
|
||||
text:
|
||||
error == null ? "Your application has been submitted" : error.message,
|
||||
text: error == null ? "Your application has been submitted" : error.message,
|
||||
confirmButtonColor: error == null ? "green" : "red",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed && applyProject) {
|
||||
@ -85,10 +83,7 @@ export default function ApplyBusiness() {
|
||||
};
|
||||
|
||||
const hasUserApplied = async (userID: string) => {
|
||||
let { data: business, error } = await supabase
|
||||
.from("business")
|
||||
.select("*")
|
||||
.eq("user_id", userID);
|
||||
let { data: business, error } = await supabase.from("business").select("*").eq("user_id", userID);
|
||||
console.table(business);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@ -100,24 +95,21 @@ export default function ApplyBusiness() {
|
||||
};
|
||||
const transformChoice = (data: any) => {
|
||||
// convert any yes and no to true or false
|
||||
const transformedData = Object.entries(data).reduce(
|
||||
(acc: Record<any, any>, [key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
const lowerValue = value.toLowerCase();
|
||||
if (lowerValue === "yes") {
|
||||
acc[key] = true;
|
||||
} else if (lowerValue === "no") {
|
||||
acc[key] = false;
|
||||
} else {
|
||||
acc[key] = value; // keep other string values unchanged
|
||||
}
|
||||
const transformedData = Object.entries(data).reduce((acc: Record<any, any>, [key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
const lowerValue = value.toLowerCase();
|
||||
if (lowerValue === "yes") {
|
||||
acc[key] = true;
|
||||
} else if (lowerValue === "no") {
|
||||
acc[key] = false;
|
||||
} 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;
|
||||
};
|
||||
useEffect(() => {
|
||||
@ -164,22 +156,16 @@ export default function ApplyBusiness() {
|
||||
</h1>
|
||||
<div className="mt-5 justify-self-center">
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
All information submitted in this application is for internal use
|
||||
only and is treated with the utmost{" "}
|
||||
All information submitted in this application is for internal use only and is treated with the utmost{" "}
|
||||
</p>
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
confidentiality. Companies may apply to raise with B2DVentures more
|
||||
than once.
|
||||
confidentiality. Companies may apply to raise with B2DVentures more than once.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* form */}
|
||||
{/* <form action="" onSubmit={handleSubmit(handleSubmitForms)}> */}
|
||||
<BusinessForm
|
||||
onSubmit={onSubmit}
|
||||
applyProject={applyProject}
|
||||
setApplyProject={setApplyProject}
|
||||
/>
|
||||
<BusinessForm onSubmit={onSubmit} applyProject={applyProject} setApplyProject={setApplyProject} />
|
||||
</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 { 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
|
||||
export function useDealList() {
|
||||
const [dealList, setDealList] = useState<Deal[]>();
|
||||
const [dealList, setDealList] = useState<Deal[]>([]);
|
||||
|
||||
const fetchDealList = async () => {
|
||||
setDealList(await getDealList());
|
||||
// set the state to the deal list of current business user
|
||||
setDealList(await getDealList(await getCurrentUserID()));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -14,4 +17,37 @@ export function useDealList() {
|
||||
}, []);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -12,22 +12,18 @@ import { Overview } from "@/components/ui/overview";
|
||||
import { RecentFunds } from "@/components/recent-funds";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useDealList } from "./hook";
|
||||
import { useDealList, useGraphData, useRecentDealData } from "./hook";
|
||||
import { sumByKey } from "@/lib/utils";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [graphType, setGraphType] = useState("line");
|
||||
const graphData = useGraphData();
|
||||
const dealList = useDealList();
|
||||
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
|
||||
// #TODO dependency injection refactor + define default value inside function (and not here)
|
||||
const recentDealData = useRecentDealData() || [];
|
||||
|
||||
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">
|
||||
<Image
|
||||
src="/examples/dashboard-light.png"
|
||||
@ -75,7 +71,7 @@ export default function Dashboard() {
|
||||
</svg>
|
||||
</CardHeader>
|
||||
<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">
|
||||
+20.1% from last month
|
||||
</p> */}
|
||||
@ -166,7 +162,7 @@ export default function Dashboard() {
|
||||
<CardTitle>Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<Overview graphType={graphType} />
|
||||
<Overview graphType={graphType} graphData={graphData} />
|
||||
{/* tab to switch between line and bar graph */}
|
||||
<Tabs
|
||||
defaultValue="line"
|
||||
@ -197,7 +193,7 @@ export default function Dashboard() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecentFunds>
|
||||
<RecentFunds recentDealData={recentDealData}>
|
||||
</RecentFunds>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
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";
|
||||
|
||||
import React, { Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ProjectCard } from "@/components/projectCard";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getBusinesses, getInvestmentCounts, getProjects, getTags } from "@/lib/data/query";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { getBusinessAndProject } from "@/lib/data/businessQuery";
|
||||
|
||||
interface ProjectInvestmentDetail extends Tables<"ProjectInvestmentDetail"> {}
|
||||
|
||||
interface Project extends Tables<"Project"> {
|
||||
ProjectInvestmentDetail: ProjectInvestmentDetail[];
|
||||
}
|
||||
|
||||
interface Business extends Tables<"Business"> {
|
||||
Projects: Project[];
|
||||
}
|
||||
|
||||
export default function Find() {
|
||||
function FindContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const query = searchParams.get("query");
|
||||
// const query = "neon";
|
||||
|
||||
let supabase = createSupabaseClient();
|
||||
|
||||
@ -31,95 +19,71 @@ export default function Find() {
|
||||
data: businesses,
|
||||
isLoading: isLoadingBusinesses,
|
||||
error: businessError,
|
||||
} = useQuery(getBusinesses(supabase, query));
|
||||
} = useQuery(getBusinessAndProject(supabase, { businessName: query }));
|
||||
|
||||
const businessIds = businesses?.map((b) => b.id) || [];
|
||||
const {
|
||||
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,
|
||||
})) || [],
|
||||
})) || [];
|
||||
const isLoading = isLoadingBusinesses;
|
||||
const error = businessError;
|
||||
|
||||
if (isLoading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error fetching data: {error.message}</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-10 mx-[15%]">
|
||||
<div className="container max-w-screen-xl">
|
||||
<div className="mt-4">
|
||||
<h1 className="text-4xl font-bold">Result</h1>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{results.length === 0 && <p>No results found.</p>}
|
||||
{results.length > 0 && (
|
||||
<ul>
|
||||
{results.map((business) => (
|
||||
<li key={business.id}>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{business.businessName}</CardTitle>
|
||||
<CardDescription>Joined Date: {new Date(business.joinedDate).toLocaleDateString()}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{business.Projects.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
name={project.projectName}
|
||||
description={project.projectName}
|
||||
joinDate={project.projectName}
|
||||
location={"Bangkok"}
|
||||
minInvestment={project.ProjectInvestmentDetail[0]?.minInvestment}
|
||||
totalInvestor={project.ProjectInvestmentDetail[0]?.totalInvestment}
|
||||
totalRaised={project.ProjectInvestmentDetail[0]?.targetInvestment}
|
||||
tags={[]}
|
||||
imageUri={null}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<Card className="w-full">
|
||||
<CardContent className="my-2">
|
||||
{businesses!.length === 0 && <p>No results found.</p>}
|
||||
{businesses!.length > 0 && (
|
||||
<ul>
|
||||
{businesses!.map((business) => (
|
||||
<li key={business.business_id}>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{business.business_name}</CardTitle>
|
||||
<CardDescription>
|
||||
Joined Date: {new Date(business.joined_date).toLocaleDateString()}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-4 gap-4">
|
||||
{business?.projects && business.projects.length > 0 ? (
|
||||
business.projects.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
name={project.project_name}
|
||||
description={project.project_short_description}
|
||||
joinDate={project.published_time}
|
||||
location={business.location}
|
||||
minInvestment={project.min_investment}
|
||||
totalInvestor={project.total_investment}
|
||||
totalRaised={project.target_investment}
|
||||
tags={project.tags?.map((tag) => String(tag.tag_value)) || []}
|
||||
imageUri={project.card_image_url}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p>No Projects</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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 { Montserrat } from "next/font/google";
|
||||
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,191 +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 { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ProjectCard } from "@/components/projectCard";
|
||||
import { getTopProjects } from "@/lib/data/projectQuery";
|
||||
@ -47,17 +41,12 @@ const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
|
||||
imageUri={project.card_image_url}
|
||||
joinDate={new Date(project.published_time).toLocaleDateString()}
|
||||
location={project.business[0]?.location || ""}
|
||||
tags={project.project_tag.flatMap(
|
||||
(item: { tag: { id: number; value: string }[] }) =>
|
||||
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
|
||||
tags={project.project_tag.flatMap((item: { tag: { id: number; value: string }[] }) =>
|
||||
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
|
||||
)}
|
||||
minInvestment={
|
||||
project.project_investment_detail[0]?.min_investment || 0
|
||||
}
|
||||
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
|
||||
totalInvestor={0}
|
||||
totalRaised={
|
||||
project.project_investment_detail[0]?.total_investment || 0
|
||||
}
|
||||
totalRaised={project.project_investment_detail[0]?.total_investment || 0}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
@ -68,17 +57,13 @@ const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
|
||||
const ProjectsLoader = () => (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-64 bg-gray-200 animate-pulse rounded-lg"
|
||||
></div>
|
||||
<div key={index} className="h-64 bg-gray-200 animate-pulse rounded-lg"></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
export default async function Home() {
|
||||
const supabase = createSupabaseClient();
|
||||
const { data: topProjectsData, error: topProjectsError } =
|
||||
await getTopProjects(supabase);
|
||||
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
|
||||
|
||||
return (
|
||||
<main>
|
||||
@ -87,14 +72,9 @@ export default async function Home() {
|
||||
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
|
||||
<div className="container max-w-screen-xl flex flex-col">
|
||||
<span className="mx-20 px-10 py-10">
|
||||
<p className="text-4xl font-bold">
|
||||
Explore the world of ventures
|
||||
</p>
|
||||
<p className="text-4xl font-bold">Explore the world of ventures</p>
|
||||
<span className="text-lg">
|
||||
<p>
|
||||
Unlock opportunities and connect with a community of
|
||||
passionate
|
||||
</p>
|
||||
<p>Unlock opportunities and connect with a community of passionate</p>
|
||||
<p>investors and innovators.</p>
|
||||
<p>Together, we turn ideas into impact.</p>
|
||||
</span>
|
||||
@ -142,23 +122,11 @@ export default async function Home() {
|
||||
</CardHeader>
|
||||
<CardContent className="flex gap-2">
|
||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||
<Image
|
||||
src={"/github.svg"}
|
||||
width={20}
|
||||
height={20}
|
||||
alt="github"
|
||||
className="scale-75 md:scale-100"
|
||||
/>
|
||||
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||
Github
|
||||
</Button>
|
||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||
<Image
|
||||
src={"/github.svg"}
|
||||
width={20}
|
||||
height={20}
|
||||
alt="github"
|
||||
className="scale-75 md:scale-100"
|
||||
/>
|
||||
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||
Github
|
||||
</Button>
|
||||
</CardContent>
|
||||
@ -170,13 +138,15 @@ export default async function Home() {
|
||||
<div className="flex flex-col px-10">
|
||||
<span className="pb-5">
|
||||
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
||||
<p className="text-md md:text-lg">
|
||||
The deals attracting the most interest right now
|
||||
</p>
|
||||
<p className="text-md md:text-lg">The deals attracting the most interest right now</p>
|
||||
</span>
|
||||
<Suspense fallback={<ProjectsLoader />}>
|
||||
<TopProjects projects={topProjectsData || []} />
|
||||
</Suspense>
|
||||
{topProjectsError ? (
|
||||
<div className="text-red-500">Error fetching projects: {topProjectsError}</div>
|
||||
) : (
|
||||
<Suspense fallback={<ProjectsLoader />}>
|
||||
<TopProjects projects={topProjectsData || []} />
|
||||
</Suspense>
|
||||
)}
|
||||
<div className="self-center py-5 scale-75 md:scale-100">
|
||||
<Button>
|
||||
<Link href={"/deals"}>View all</Link>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import ProjectForm from "@/components/ProjectForm";
|
||||
import { projectFormSchema } from "@/types/schemas/application.schema";
|
||||
@ -17,7 +18,7 @@ const BUCKET_PITCH_APPLICATION_NAME = "project-application";
|
||||
export default function ApplyProject() {
|
||||
const [isSuccess, setIsSuccess] = useState(true);
|
||||
const onSubmit: SubmitHandler<projectSchema> = async (data) => {
|
||||
alert("มาแน้ววว");
|
||||
// alert("มาแน้ววว");
|
||||
await sendApplication(data);
|
||||
// console.table(data);
|
||||
// console.log(typeof data["projectPhotos"], data["projectPhotos"]);
|
||||
@ -29,8 +30,7 @@ export default function ApplyProject() {
|
||||
.insert([
|
||||
{
|
||||
user_id: userId,
|
||||
pitch_deck_url:
|
||||
pitchType === "string" ? recvData["projectPitchDeck"] : "",
|
||||
pitch_deck_url: pitchType === "string" ? recvData["projectPitchDeck"] : "",
|
||||
target_investment: recvData["targetInvest"],
|
||||
deadline: recvData["deadline"],
|
||||
project_name: recvData["projectName"],
|
||||
@ -58,36 +58,21 @@ export default function ApplyProject() {
|
||||
const results = await Promise.all(tagPromises);
|
||||
|
||||
// Collect errors
|
||||
const errors = results
|
||||
.filter((result) => result.error)
|
||||
.map((result) => result.error);
|
||||
const errors = results.filter((result) => result.error).map((result) => result.error);
|
||||
|
||||
return { errors };
|
||||
};
|
||||
|
||||
const uploadPitchFile = async (
|
||||
file: File,
|
||||
userId: string,
|
||||
projectId: string
|
||||
) => {
|
||||
const uploadPitchFile = async (file: File, userId: string, projectId: string) => {
|
||||
if (!file || !userId) {
|
||||
console.error("Pitch file or user ID is undefined.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return await uploadFile(
|
||||
file,
|
||||
BUCKET_PITCH_APPLICATION_NAME,
|
||||
`${userId}/${projectId}/pitches/${file.name}`
|
||||
);
|
||||
return await uploadFile(file, BUCKET_PITCH_APPLICATION_NAME, `${userId}/${projectId}/pitches/${file.name}`);
|
||||
};
|
||||
|
||||
const uploadLogoAndPhotos = async (
|
||||
logoFile: File,
|
||||
photos: File[],
|
||||
userId: string,
|
||||
projectId: string
|
||||
) => {
|
||||
const uploadLogoAndPhotos = async (logoFile: File, photos: File[], userId: string, projectId: string) => {
|
||||
const uploadResults: { logo?: any; photos: any[] } = { photos: [] };
|
||||
|
||||
// upload logo
|
||||
@ -108,11 +93,7 @@ export default function ApplyProject() {
|
||||
|
||||
// upload each photo
|
||||
const uploadPhotoPromises = photos.map((image) =>
|
||||
uploadFile(
|
||||
image,
|
||||
BUCKET_PITCH_APPLICATION_NAME,
|
||||
`${userId}/${projectId}/photos/${image.name}`
|
||||
)
|
||||
uploadFile(image, BUCKET_PITCH_APPLICATION_NAME, `${userId}/${projectId}/photos/${image.name}`)
|
||||
);
|
||||
|
||||
const photoResults = await Promise.all(uploadPhotoPromises);
|
||||
@ -132,8 +113,7 @@ export default function ApplyProject() {
|
||||
Swal.fire({
|
||||
icon: error == null ? "success" : "error",
|
||||
title: error == null ? "Success" : `Error: ${error.code}`,
|
||||
text:
|
||||
error == null ? "Your application has been submitted" : error.message,
|
||||
text: error == null ? "Your application has been submitted" : error.message,
|
||||
confirmButtonColor: error == null ? "green" : "red",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
@ -168,11 +148,7 @@ export default function ApplyProject() {
|
||||
|
||||
// upload pitch file if it’s a file
|
||||
if (typeof recvData["projectPitchDeck"] === "object") {
|
||||
const uploadPitchSuccess = await uploadPitchFile(
|
||||
recvData["projectPitchDeck"],
|
||||
user.id,
|
||||
projectId
|
||||
);
|
||||
const uploadPitchSuccess = await uploadPitchFile(recvData["projectPitchDeck"], user.id, projectId);
|
||||
|
||||
if (!uploadPitchSuccess) {
|
||||
console.error("Error uploading pitch file.");
|
||||
@ -196,21 +172,11 @@ export default function ApplyProject() {
|
||||
// console.log("Logo Path:", logo.data.path);
|
||||
// console.table(photos);
|
||||
|
||||
const logoURL = await getPrivateURL(
|
||||
logo.data.path,
|
||||
BUCKET_PITCH_APPLICATION_NAME
|
||||
);
|
||||
const logoURL = await getPrivateURL(logo.data.path, BUCKET_PITCH_APPLICATION_NAME);
|
||||
let photoURLsArray: string[] = [];
|
||||
const photoURLPromises = photos.map(
|
||||
async (item: {
|
||||
success: boolean;
|
||||
errors: typeof errors;
|
||||
data: { path: string };
|
||||
}) => {
|
||||
const photoURL = await getPrivateURL(
|
||||
item.data.path,
|
||||
BUCKET_PITCH_APPLICATION_NAME
|
||||
);
|
||||
async (item: { success: boolean; errors: typeof errors; data: { path: string } }) => {
|
||||
const photoURL = await getPrivateURL(item.data.path, BUCKET_PITCH_APPLICATION_NAME);
|
||||
if (photoURL?.signedUrl) {
|
||||
photoURLsArray.push(photoURL.signedUrl);
|
||||
} else {
|
||||
@ -233,11 +199,7 @@ export default function ApplyProject() {
|
||||
setIsSuccess(true);
|
||||
displayAlert(error);
|
||||
};
|
||||
const updateImageURL = async (
|
||||
url: string | string[],
|
||||
columnName: string,
|
||||
projectId: number
|
||||
) => {
|
||||
const updateImageURL = async (url: string | string[], columnName: string, projectId: number) => {
|
||||
const { error } = await supabase
|
||||
.from("project_application")
|
||||
.update({ [columnName]: url })
|
||||
@ -250,9 +212,7 @@ export default function ApplyProject() {
|
||||
}
|
||||
};
|
||||
const getPrivateURL = async (path: string, bucketName: string) => {
|
||||
const { data } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.createSignedUrl(path, 9999999999999999999999999999);
|
||||
const { data } = await supabase.storage.from(bucketName).createSignedUrl(path, 9999999999999999999999999999);
|
||||
// console.table(data);
|
||||
return data;
|
||||
};
|
||||
@ -265,12 +225,10 @@ export default function ApplyProject() {
|
||||
</h1>
|
||||
<div className="mt-5 justify-self-center">
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
Begin Your First Fundraising Project. Starting a fundraising project
|
||||
is mandatory for all businesses.
|
||||
Begin Your First Fundraising Project. Starting a fundraising project is mandatory for all businesses.
|
||||
</p>
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
This step is crucial to begin your journey and unlock the necessary
|
||||
tools for raising funds.
|
||||
This step is crucial to begin your journey and unlock the necessary tools for raising funds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,27 +3,14 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DualOptionSelector } from "@/components/dualSelector";
|
||||
import { MultipleOptionSelector } from "@/components/multipleSelector";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { businessFormSchema } from "@/types/schemas/application.schema";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@radix-ui/react-tooltip";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
|
||||
type businessSchema = z.infer<typeof businessFormSchema>;
|
||||
@ -54,14 +41,10 @@ const BusinessForm = ({
|
||||
let supabase = createSupabaseClient();
|
||||
const [businessPitch, setBusinessPitch] = useState("text");
|
||||
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 fetchIndustry = async () => {
|
||||
let { data: BusinessType, error } = await supabase
|
||||
.from("business_type")
|
||||
.select("id, value");
|
||||
let { data: BusinessType, error } = await supabase.from("business_type").select("id, value");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@ -84,18 +67,12 @@ const BusinessForm = ({
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
const countryList = data.map(
|
||||
(country: { name: { common: string } }, index: number) => ({
|
||||
id: index + 1,
|
||||
name: country.name.common,
|
||||
})
|
||||
);
|
||||
const countryList = data.map((country: { name: { common: string } }, index: number) => ({
|
||||
id: index + 1,
|
||||
name: country.name.common,
|
||||
}));
|
||||
|
||||
setCountries(
|
||||
countryList.sort((a: { name: string }, b: { name: any }) =>
|
||||
a.name.localeCompare(b.name)
|
||||
)
|
||||
);
|
||||
setCountries(countryList.sort((a: { name: string }, b: { name: any }) => a.name.localeCompare(b.name)));
|
||||
} catch (error) {
|
||||
console.error("Error fetching countries:", error);
|
||||
}
|
||||
@ -106,15 +83,11 @@ const BusinessForm = ({
|
||||
}, []);
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<businessSchema>)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<form 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%] ">
|
||||
<h1 className="text-3xl font-bold mt-10 ml-96">About your company</h1>
|
||||
<p className="ml-96 mt-5 text-neutral-500">
|
||||
<span className="text-red-500 font-bold">**</span>All requested
|
||||
information in this section is required.
|
||||
<span className="text-red-500 font-bold">**</span>All requested information in this section is required.
|
||||
</p>
|
||||
<div className="ml-96 mt-5 space-y-10">
|
||||
{/* Company Name */}
|
||||
@ -123,21 +96,13 @@ const BusinessForm = ({
|
||||
name="companyName"
|
||||
render={({ field }: { field: any }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="font-bold text-lg">
|
||||
Company name
|
||||
</FormLabel>
|
||||
<FormLabel className="font-bold text-lg">Company name</FormLabel>
|
||||
<FormControl>
|
||||
<div className="mt-10 space-y-5">
|
||||
<div className="flex space-x-5">
|
||||
<Input
|
||||
type="text"
|
||||
id="companyName"
|
||||
className="w-96"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="text" id="companyName" className="w-96" {...field} />
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
This should be the name your company uses on your{" "}
|
||||
<br />
|
||||
This should be the name your company uses on your <br />
|
||||
website and in the market.
|
||||
</span>
|
||||
</div>
|
||||
@ -162,9 +127,7 @@ const BusinessForm = ({
|
||||
// console.log("Country selected: " + selectedValues.name);
|
||||
field.onChange(selectedValues.name);
|
||||
}}
|
||||
description={
|
||||
<>Select the country where your business is based.</>
|
||||
}
|
||||
description={<>Select the country where your business is based.</>}
|
||||
placeholder="Select a country"
|
||||
selectLabel="Country"
|
||||
/>
|
||||
@ -189,12 +152,7 @@ const BusinessForm = ({
|
||||
// console.log("Type of selected value:", selectedValues.id);
|
||||
field.onChange(selectedValues.id);
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
Choose the industry that best aligns with your
|
||||
business.
|
||||
</>
|
||||
}
|
||||
description={<>Choose the industry that best aligns with your business.</>}
|
||||
placeholder="Select an industry"
|
||||
selectLabel="Industry"
|
||||
/>
|
||||
@ -229,8 +187,7 @@ const BusinessForm = ({
|
||||
value={field.value}
|
||||
/>
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
The sum total of past financing, including angel or
|
||||
venture <br />
|
||||
The sum total of past financing, including angel or venture <br />
|
||||
capital, loans, grants, or token sales.
|
||||
</span>
|
||||
</div>
|
||||
@ -251,11 +208,7 @@ const BusinessForm = ({
|
||||
<div className="flex space-x-5">
|
||||
<DualOptionSelector
|
||||
name="isInUS"
|
||||
label={
|
||||
<>
|
||||
Is your company incorporated in the United States?
|
||||
</>
|
||||
}
|
||||
label={<>Is your company incorporated in the United States?</>}
|
||||
choice1="Yes"
|
||||
choice2="No"
|
||||
handleFunction={(selectedValues: string) => {
|
||||
@ -266,8 +219,7 @@ const BusinessForm = ({
|
||||
value={field.value}
|
||||
/>
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
Only companies that are incorporated or formed in the US
|
||||
are eligible to raise via Reg CF.
|
||||
Only companies that are incorporated or formed in the US are eligible to raise via Reg CF.
|
||||
</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
@ -287,21 +239,14 @@ const BusinessForm = ({
|
||||
<DualOptionSelector
|
||||
name="isForSale"
|
||||
value={field.value}
|
||||
label={
|
||||
<>Is your product available (for sale) in market?</>
|
||||
}
|
||||
label={<>Is your product available (for sale) in market?</>}
|
||||
choice1="Yes"
|
||||
choice2="No"
|
||||
handleFunction={(selectedValues: string) => {
|
||||
// setIsForSale;
|
||||
field.onChange(selectedValues);
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
Only check this box if customers can access, use, or
|
||||
buy your product today.
|
||||
</>
|
||||
}
|
||||
description={<>Only check this box if customers can access, use, or buy your product today.</>}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
@ -328,10 +273,7 @@ const BusinessForm = ({
|
||||
field.onChange(selectedValues);
|
||||
}}
|
||||
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>
|
||||
@ -356,9 +298,7 @@ const BusinessForm = ({
|
||||
<div className="flex space-x-2 w-96">
|
||||
<Button
|
||||
type="button"
|
||||
variant={
|
||||
businessPitch === "text" ? "default" : "outline"
|
||||
}
|
||||
variant={businessPitch === "text" ? "default" : "outline"}
|
||||
onClick={() => setBusinessPitch("text")}
|
||||
className="w-32 h-12 text-base"
|
||||
>
|
||||
@ -366,9 +306,7 @@ const BusinessForm = ({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={
|
||||
businessPitch === "file" ? "default" : "outline"
|
||||
}
|
||||
variant={businessPitch === "file" ? "default" : "outline"}
|
||||
onClick={() => setBusinessPitch("file")}
|
||||
className="w-32 h-12 text-base"
|
||||
>
|
||||
@ -378,14 +316,8 @@ const BusinessForm = ({
|
||||
<div className="flex space-x-5">
|
||||
<Input
|
||||
type={businessPitch === "file" ? "file" : "text"}
|
||||
placeholder={
|
||||
businessPitch === "file"
|
||||
? "Upload your Markdown file"
|
||||
: "https:// "
|
||||
}
|
||||
accept={
|
||||
businessPitch === "file" ? ".md" : undefined
|
||||
}
|
||||
placeholder={businessPitch === "file" ? "Upload your Markdown file" : "https:// "}
|
||||
accept={businessPitch === "file" ? ".md" : undefined}
|
||||
onChange={(e) => {
|
||||
const value = e.target;
|
||||
if (businessPitch === "file") {
|
||||
@ -399,17 +331,12 @@ const BusinessForm = ({
|
||||
/>
|
||||
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
Your pitch deck and other application info will be
|
||||
used for <br />
|
||||
Your pitch deck and other application info will be used for <br />
|
||||
internal purposes only. <br />
|
||||
Please make sure this document is publicly
|
||||
accessible. This can <br />
|
||||
be a DocSend, Box, Dropbox, Google Drive or other
|
||||
link.
|
||||
Please make sure this document is publicly accessible. This can <br />
|
||||
be a DocSend, Box, Dropbox, Google Drive or other link.
|
||||
<br />
|
||||
<p className="text-red-500">
|
||||
** support only markdown(.md) format
|
||||
</p>
|
||||
<p className="text-red-500">** support only markdown(.md) format</p>
|
||||
</span>
|
||||
</div>
|
||||
{businessPitchFile && (
|
||||
@ -448,10 +375,7 @@ const BusinessForm = ({
|
||||
field.onChange(selectedValues.name);
|
||||
}}
|
||||
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"
|
||||
selectLabel="Select"
|
||||
@ -462,32 +386,25 @@ const BusinessForm = ({
|
||||
)}
|
||||
/>
|
||||
<div className="flex space-x-5">
|
||||
<Switch
|
||||
onCheckedChange={() => setApplyProject(!applyProject)}
|
||||
></Switch>
|
||||
<Switch onCheckedChange={() => setApplyProject(!applyProject)}></Switch>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-[12px] text-neutral-500 self-center cursor-pointer">
|
||||
Would you like to apply for your first fundraising project
|
||||
as well?
|
||||
Would you like to apply for your first fundraising project as well?
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-[11px]">
|
||||
Toggling this option allows you to begin your first
|
||||
project, <br /> which is crucial for unlocking the tools
|
||||
necessary to raise funds.
|
||||
Toggling this option allows you to begin your first project, <br /> which is crucial for unlocking
|
||||
the tools necessary to raise funds.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<center>
|
||||
<Button
|
||||
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5"
|
||||
type="submit"
|
||||
>
|
||||
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5" type="submit">
|
||||
Submit application
|
||||
</Button>
|
||||
</center>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
export const Icons = {
|
||||
userLogo: (props: IconProps) => (
|
||||
userLogo: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{...props}
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">{title}</div>
|
||||
<hr />
|
||||
<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 (
|
||||
<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">
|
||||
@ -71,9 +65,10 @@ export function NavigationBar() {
|
||||
<Link
|
||||
className="flex-none text-xl font-semibold dark:text-white focus:outline-none focus:opacity-80"
|
||||
href="/"
|
||||
aria-label="Brand">
|
||||
aria-label="Brand"
|
||||
>
|
||||
<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
|
||||
</span>
|
||||
</Link>
|
||||
@ -108,25 +103,6 @@ export function NavigationBar() {
|
||||
</NavigationMenuContent>
|
||||
</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">
|
||||
<SearchBar />
|
||||
</NavigationMenuItem>
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
@ -16,6 +15,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Bell, Heart, Wallet } from "lucide-react";
|
||||
import { LogoutButton } from "@/components/auth/logoutButton";
|
||||
import useSession from "@/lib/supabase/useSession";
|
||||
import { useUserRole } from "@/hooks/useUserRole";
|
||||
|
||||
const UnAuthenticatedComponents = () => {
|
||||
return (
|
||||
@ -35,8 +35,13 @@ const UnAuthenticatedComponents = () => {
|
||||
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
||||
let notifications = 100;
|
||||
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 (
|
||||
<div className="flex gap-3 pl-2 items-center">
|
||||
<div className={`flex gap-3 pl-2 items-center ${businessClass}`}>
|
||||
<Link href={"/notification"}>
|
||||
<div className="relative inline-block">
|
||||
<Bell className="h-6 w-6" />
|
||||
@ -63,6 +68,11 @@ const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
{data != null && data != undefined && data.role === "admin" && (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/admin">Admin</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogoutButton />
|
||||
|
||||
@ -1,25 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "./ui/icons";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
|
||||
export function CardsPaymentMethod() {
|
||||
return (
|
||||
@ -33,12 +19,7 @@ export function CardsPaymentMethod() {
|
||||
<CardContent className="grid gap-6">
|
||||
<RadioGroup defaultValue="card" className="flex w-full justify-center gap-1 md:gap-3">
|
||||
<div className="w-[100px] lg:w-[130px]">
|
||||
<RadioGroupItem
|
||||
value="card"
|
||||
id="card"
|
||||
className="peer sr-only"
|
||||
aria-label="Card"
|
||||
/>
|
||||
<RadioGroupItem value="card" id="card" className="peer sr-only" aria-label="Card" />
|
||||
<Label
|
||||
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"
|
||||
@ -61,12 +42,7 @@ export function CardsPaymentMethod() {
|
||||
</div>
|
||||
|
||||
<div className="w-[100px] lg:w-[130px]">
|
||||
<RadioGroupItem
|
||||
value="paypal"
|
||||
id="paypal"
|
||||
className="peer sr-only"
|
||||
aria-label="Paypal"
|
||||
/>
|
||||
<RadioGroupItem value="paypal" id="paypal" className="peer sr-only" aria-label="Paypal" />
|
||||
<Label
|
||||
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"
|
||||
@ -77,12 +53,7 @@ export function CardsPaymentMethod() {
|
||||
</div>
|
||||
|
||||
<div className="w-[100px] lg:w-[130px]">
|
||||
<RadioGroupItem
|
||||
value="apple"
|
||||
id="apple"
|
||||
className="peer sr-only"
|
||||
aria-label="Apple"
|
||||
/>
|
||||
<RadioGroupItem value="apple" id="apple" className="peer sr-only" aria-label="Apple" />
|
||||
<Label
|
||||
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"
|
||||
|
||||
@ -27,7 +27,8 @@ export function ProjectCard(props: ProjectCardProps) {
|
||||
className={cn(
|
||||
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
|
||||
props.className
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Image */}
|
||||
<div className="relative h-3/4 w-full">
|
||||
@ -69,11 +70,15 @@ export function ProjectCard(props: ProjectCardProps) {
|
||||
<span className="text-xs">{props.location}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap mt-1 items-center text-muted-foreground">
|
||||
{props.tags.map((tag) => (
|
||||
<span id="tag" key={tag} className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{props.tags && Array.isArray(props.tags) ? (
|
||||
props.tags.map((tag) => (
|
||||
<span id="tag" key={tag} className="text-[10px] rounded-md bg-slate-200 dark:bg-slate-700 p-1 mr-1">
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">No tags available</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,59 +1,39 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Olivia Martin",
|
||||
email: "olivia.martin@email.com",
|
||||
amount: "1900.00",
|
||||
avatar: "/avatars/01.png", // psuedo avatar image
|
||||
initials: "OM",
|
||||
},
|
||||
{
|
||||
name: "Jackson Lee",
|
||||
email: "jackson.lee@email.com",
|
||||
amount: "39.00",
|
||||
avatar: "/avatars/02.png",
|
||||
initials: "JL",
|
||||
},
|
||||
{
|
||||
name: "Isabella Nguyen",
|
||||
email: "isabella.nguyen@email.com",
|
||||
amount: "299.00",
|
||||
avatar: "/avatars/03.png",
|
||||
initials: "IN",
|
||||
},
|
||||
{
|
||||
name: "William Kim",
|
||||
email: "will@email.com",
|
||||
amount: "99.00",
|
||||
avatar: "/avatars/04.png",
|
||||
initials: "WK",
|
||||
},
|
||||
{
|
||||
name: "Sofia Davis",
|
||||
email: "sofia.davis@email.com",
|
||||
amount: "39.00",
|
||||
avatar: "/avatars/05.png",
|
||||
initials: "SD",
|
||||
},
|
||||
];
|
||||
export type RecentDealData = {
|
||||
created_time: Date;
|
||||
deal_amount: number;
|
||||
investor_id: string;
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
// email: string;
|
||||
};
|
||||
|
||||
export function RecentFunds() {
|
||||
interface RecentFundsProps {
|
||||
recentDealData: RecentDealData[];
|
||||
}
|
||||
|
||||
export function RecentFunds({ recentDealData }: RecentFundsProps) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{data.map((person, index) => (
|
||||
<div className="flex items-center" key={index}>
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src={person.avatar} alt={person.name} />
|
||||
<AvatarFallback>{person.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{person.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{person.email}</p>
|
||||
{recentDealData?.length > 0 ? (
|
||||
recentDealData.map((data) => (
|
||||
<div className="flex items-center" key={data.investor_id}>
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src={data.avatar_url} alt={data.username} />
|
||||
{/* #TODO make this not quick fix */}
|
||||
<AvatarFallback>{data.username ? data.username[0]: ""}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="ml-4 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{data.username}</p>
|
||||
{/* <p className="text-sm text-muted-foreground">{data.email}</p> */}
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+${data.deal_amount}</div>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+${person.amount}</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<p>No recent deals available.</p>
|
||||
)}
|
||||
</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 };
|
||||
@ -2,66 +2,22 @@
|
||||
|
||||
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, LineChart, Line } from "recharts";
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Jan",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Feb",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Mar",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Apr",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "May",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Jun",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Jul",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Aug",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Sep",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Oct",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Nov",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
{
|
||||
name: "Dec",
|
||||
total: Math.floor(Math.random() * 5000) + 1000,
|
||||
},
|
||||
];
|
||||
|
||||
interface OverViewProps{
|
||||
graphType:string;
|
||||
interface OverViewProps {
|
||||
graphType: string;
|
||||
graphData: Record<string, number>; // Object with month-year as keys and sum as value
|
||||
}
|
||||
|
||||
export function Overview(props: OverViewProps) {
|
||||
// Transform the grouped data into the format for the chart
|
||||
const chartData = Object.entries(props.graphData).map(([monthYear, totalArray]) => ({
|
||||
name: monthYear,
|
||||
total: totalArray, // Get the total amount for the month
|
||||
}));
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
{props.graphType === 'line' ? (
|
||||
<LineChart data={data}>
|
||||
<LineChart data={chartData}>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#888888"
|
||||
@ -83,7 +39,7 @@ export function Overview(props: OverViewProps) {
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<BarChart data={data}>
|
||||
<BarChart data={chartData}>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#888888"
|
||||
@ -106,5 +62,5 @@ export function Overview(props: OverViewProps) {
|
||||
</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";
|
||||
|
||||
async function getTopProjects(
|
||||
client: SupabaseClient,
|
||||
numberOfRecords: number = 4
|
||||
) {
|
||||
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
||||
try {
|
||||
const { data, error } = await client
|
||||
.from("project")
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
project_name,
|
||||
business_id,
|
||||
published_time,
|
||||
project_short_description,
|
||||
card_image_url,
|
||||
project_investment_detail (
|
||||
min_investment,
|
||||
total_investment,
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
project_tag (
|
||||
tag (
|
||||
id,
|
||||
value
|
||||
)
|
||||
),
|
||||
business (
|
||||
location
|
||||
id,
|
||||
project_name,
|
||||
business_id,
|
||||
published_time,
|
||||
project_short_description,
|
||||
card_image_url,
|
||||
project_investment_detail (
|
||||
min_investment,
|
||||
total_investment,
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
project_tag (
|
||||
tag (
|
||||
id,
|
||||
value
|
||||
)
|
||||
`
|
||||
),
|
||||
business (
|
||||
location
|
||||
)
|
||||
`
|
||||
)
|
||||
.order("published_time", { ascending: false })
|
||||
.limit(numberOfRecords);
|
||||
@ -62,7 +59,7 @@ function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
tags:item_tag!inner (
|
||||
tags:project_tag!inner (
|
||||
...tag!inner (
|
||||
tag_name:value
|
||||
)
|
||||
@ -82,16 +79,20 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
||||
project_short_description,
|
||||
project_description,
|
||||
published_time,
|
||||
card_image_url,
|
||||
...project_investment_detail!inner (
|
||||
min_investment,
|
||||
total_investment,
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
tags:item_tag!inner (
|
||||
tags:project_tag!inner (
|
||||
...tag!inner (
|
||||
tag_name:value
|
||||
)
|
||||
),
|
||||
...business (
|
||||
user_id
|
||||
)
|
||||
`
|
||||
)
|
||||
@ -129,7 +130,7 @@ function searchProjectsQuery(
|
||||
}: FilterProjectQueryParams
|
||||
) {
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize - 1;
|
||||
const end = start + pageSize;
|
||||
|
||||
let query = client
|
||||
.from("project")
|
||||
@ -149,7 +150,7 @@ function searchProjectsQuery(
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
tags:item_tag!inner (
|
||||
tags:project_tag!inner (
|
||||
...tag!inner (
|
||||
tag_name:value
|
||||
)
|
||||
@ -186,7 +187,7 @@ function searchProjectsQuery(
|
||||
}
|
||||
|
||||
if (tagsFilter) {
|
||||
query = query.in("item_tag.tag.value", tagsFilter);
|
||||
query = query.in("project_tag.tag.value", tagsFilter);
|
||||
}
|
||||
|
||||
if (projectStatus) {
|
||||
@ -200,9 +201,48 @@ function searchProjectsQuery(
|
||||
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 {
|
||||
getProjectData,
|
||||
getProjectDataQuery,
|
||||
getTopProjects,
|
||||
searchProjectsQuery,
|
||||
getProjectByBusinessId,
|
||||
getProjectByUserId,
|
||||
};
|
||||
|
||||
@ -1,44 +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);
|
||||
}
|
||||
|
||||
export { getBusinesses, getInvestmentCounts, getProjects, getTags };
|
||||
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();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export function createSupabaseClient() {
|
||||
@ -12,7 +12,9 @@ export function createSupabaseClient() {
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
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[]) {
|
||||
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,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user