Merge pull request #66 from Sosokker/front-end

Business and Application form UI + logic
This commit is contained in:
Sirin Puenggun 2024-10-27 22:54:13 +07:00 committed by GitHub
commit 548e9babe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 7059 additions and 937 deletions

728
package-lock.json generated
View File

@ -9,12 +9,14 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1",
@ -33,6 +35,7 @@
"b2d-ventures": "file:",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.2.0",
@ -44,9 +47,11 @@
"react-dom": "^18",
"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",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@ -55,9 +60,13 @@
"@playwright/test": "^1.47.2",
"@tailwindcss/typography": "^0.5.15",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/next": "^8.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-fade-in": "^2.0.2",
"@types/react-lottie": "^1.2.10",
"@types/react-select-country-list": "^2.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
@ -932,6 +941,33 @@
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz",
"integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
@ -1330,6 +1366,42 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
"integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
@ -2235,6 +2307,32 @@
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/next": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@types/next/-/next-8.0.7.tgz",
"integrity": "sha512-I/Gcj1YfOFmpBBX5XgBP1t1wKcFS0TGk8ytW99ujjvCp8U31QuKqM3fvvGb7+Hf1CJt3BAAgzGT0aCigqO5opQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/next-server": "*",
"@types/node": "*",
"@types/node-fetch": "*",
"@types/react": "*"
}
},
"node_modules/@types/next-server": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/next-server/-/next-server-8.1.2.tgz",
"integrity": "sha512-Fm4QhAxwDlC9AHiGy23Lhv7DeTTt1O1s7tnAsyVOLPjePmYXPZVbOCrxd2oRHZnIIYWw41JelLbq4hN1B5idlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/next": "*",
"@types/node": "*",
"@types/react": "*",
"@types/react-loadable": "*"
}
},
"node_modules/@types/node": {
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
@ -2243,6 +2341,17 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
@ -2271,11 +2380,151 @@
"@types/react": "*"
}
},
"node_modules/@types/react-fade-in": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/react-fade-in/-/react-fade-in-2.0.2.tgz",
"integrity": "sha512-JdyLYFtyvqDP7mqnKaAyuYD+VMtzAHbUf3kumNQV5QALxjBGmb95HXD0uug1bGol053dtV5yO3NNpGHOMj413g==",
"deprecated": "This is a stub types definition. react-fade-in provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"react-fade-in": "*"
}
},
"node_modules/@types/react-fade-in/node_modules/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/react-fade-in/node_modules/react-fade-in": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-fade-in/-/react-fade-in-2.0.1.tgz",
"integrity": "sha512-oqS/WT4znaXEHmL+yo0IDUDY7uC9K4RP35j1SdRUEBspR09B2iIC0i8oJ28tPOr6Ez/L2aktF9p89j+DbsTVNw==",
"dev": true,
"peerDependencies": {
"react": "^16.8 || 17"
}
},
"node_modules/@types/react-loadable": {
"version": "5.5.11",
"resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.11.tgz",
"integrity": "sha512-/tq2IJ853MoIFRBmqVOxnGsRRjER5TmEKzsZtaAkiXAWoDeKgR/QNOT1vd9k0p9h/F616X21cpNh3hu4RutzRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"@types/webpack": "^4"
}
},
"node_modules/@types/react-lottie": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/react-lottie/-/react-lottie-1.2.10.tgz",
"integrity": "sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-select-country-list": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@types/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
"integrity": "sha512-nffcYOwuun+5B0EWqubK+amHpPdK9Xj20xkLYNqYrzmESd8FnpLwHsS79ClLAWA9y+icVA8gWPkbwBp1gpjSwA==",
"dev": true
},
"node_modules/@types/source-list-map": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz",
"integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tapable": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz",
"integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/uglify-js": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz",
"integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"source-map": "^0.6.1"
}
},
"node_modules/@types/uglify-js/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
"node_modules/@types/webpack": {
"version": "4.41.39",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.39.tgz",
"integrity": "sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/tapable": "^1",
"@types/uglify-js": "*",
"@types/webpack-sources": "*",
"anymatch": "^3.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/@types/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/source-list-map": "*",
"source-map": "^0.7.3"
}
},
"node_modules/@types/webpack-sources/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 8"
}
},
"node_modules/@types/webpack/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/ws": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
@ -2700,6 +2949,13 @@
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -2737,6 +2993,20 @@
"resolved": "",
"link": true
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
"dependencies": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
}
},
"node_modules/babel-runtime/node_modules/regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@ -3008,6 +3278,366 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/cmdk": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
"dependencies": {
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
"dependencies": {
"react-remove-scroll-bar": "^2.3.3",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3024,6 +3654,19 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -3055,6 +3698,13 @@
"node": ">= 0.6"
}
},
"node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
"hasInstallScript": true
},
"node_modules/core-js-pure": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.38.1.tgz",
@ -3388,6 +4038,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -4322,6 +4982,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -5492,6 +6167,11 @@
"loose-envify": "cli.js"
}
},
"node_modules/lottie-web": {
"version": "5.12.2",
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
"integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg=="
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@ -6104,6 +6784,29 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7006,6 +7709,21 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-lottie": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/react-lottie/-/react-lottie-1.2.4.tgz",
"integrity": "sha512-kBGxI+MIZGBf4wZhNCWwHkMcVP+kbpmrLWH/SkO0qCKc7D7eSPcxQbfpsmsCo8v2KCBYjuGSou+xTqK44D/jMg==",
"dependencies": {
"babel-runtime": "^6.26.0",
"lottie-web": "^5.1.3"
},
"engines": {
"npm": "^3.0.0"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/react-markdown": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
@ -7867,6 +8585,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sweetalert2": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.14.3.tgz",
"integrity": "sha512-6NuBHWJCv2gtw4y8PUXLB41hty+V6U2mKZMAvydL1IRPcORR0yuyq3cjFD/+ByrCk3muEFggbZX/x6HwmbVfbA==",
"license": "MIT",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/tailwind-merge": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",

View File

@ -10,12 +10,14 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1",
@ -34,6 +36,7 @@
"b2d-ventures": "file:",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.2.0",
@ -45,9 +48,11 @@
"react-dom": "^18",
"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",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@ -56,9 +61,13 @@
"@playwright/test": "^1.47.2",
"@tailwindcss/typography": "^0.5.15",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/next": "^8.0.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-fade-in": "^2.0.2",
"@types/react-lottie": "^1.2.10",
"@types/react-select-country-list": "^2.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",

File diff suppressed because it is too large Load Diff

View File

@ -125,7 +125,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="prose prose-sm max-w-none">
<div className="prose prose-sm max-w-none ">
<ReactMarkdown>{projectData?.project_description || "No pitch available."}</ReactMarkdown>
</div>
</CardContent>

View File

@ -1,5 +1,3 @@
// components/ProfilePage.tsx
import React from "react";
import Image from "next/image";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
@ -10,22 +8,11 @@ import ReactMarkdown from "react-markdown";
interface Profile extends Tables<"Profiles"> {}
export default async function ProfilePage() {
export default async function ProfilePage({ params }: { params: { uid: string } }) {
const supabase = createSupabaseClient();
const uid = params.uid;
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return (
<div className="flex items-center justify-center h-screen">
<p className="text-red-500">No user found!</p>
</div>
);
}
const { data: profileData, error } = await getUserProfile(supabase, user.id);
const { data: profileData, error } = await getUserProfile(supabase, uid);
if (error) {
return (

44
src/app/api/dealApi.ts Normal file
View File

@ -0,0 +1,44 @@
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { getCurrentUserID } from "./userApi";
export type Deal = {
deal_amount: number;
created_time: Date;
investor_id: string;
};
export async function getDealList() {
const supabase = createSupabaseClient();
const { data: dealData, error } = await supabase
.from('business')
.select(`
id,
project (
id,
investment_deal (
deal_amount,
created_time,
investor_id
)
)
`)
.eq('user_id', await getCurrentUserID())
.single();
if (error || !dealData) {
alert(JSON.stringify(error));
console.error('Error fetching deal list:', error);
} else {
const dealList = dealData.project[0].investment_deal;
if (!dealList.length) {
alert("No data available");
return; // Exit early if there's no data
}
// Sort the dealList by created_time in descending order
const byCreatedTimeDesc = (a: Deal, b: Deal) =>
new Date(b.created_time).getTime() - new Date(a.created_time).getTime();
return dealList.sort(byCreatedTimeDesc);
}
};

100
src/app/api/generalApi.ts Normal file
View File

@ -0,0 +1,100 @@
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import Swal from "sweetalert2";
const supabase = createSupabaseClient();
async function checkFolderExists(bucketName: string, filePath: string) {
const { data, error } = await supabase.storage
.from(bucketName)
.list(filePath);
if (error) {
console.error(`Error checking for folder: ${error.message}`);
}
return { folderData: data, folderError: error };
}
async function clearFolder(
bucketName: string,
folderData: any[],
filePath: string
) {
const errors: string[] = [];
for (const fileItem of folderData) {
const { error } = await supabase.storage
.from(bucketName)
.remove([`${filePath}/${fileItem.name}`]);
if (error) {
errors.push(`Error removing file (${fileItem.name}): ${error.message}`);
}
}
return errors;
}
async function uploadToFolder(
bucketName: string,
filePath: string,
file: File
) {
const { data, error } = await supabase.storage
.from(bucketName)
.upload(filePath, file, { upsert: true });
if (error) {
console.error(`Error uploading file: ${error.message}`);
}
return { uploadData: data, uploadError: error };
}
export async function uploadFile(
file: File,
bucketName: string,
filePath: string
) {
const errorMessages: string[] = [];
// check if the folder exists
const { folderData, folderError } = await checkFolderExists(
bucketName,
filePath
);
if (folderError) {
errorMessages.push(`Error checking for folder: ${folderError.message}`);
}
// clear the folder if it exists
if (folderData && folderData.length > 0) {
const clearErrors = await clearFolder(bucketName, folderData, filePath);
errorMessages.push(...clearErrors);
}
// upload the new file if there were no previous errors
let uploadData = null;
if (errorMessages.length === 0) {
const { uploadData: data, uploadError } = await uploadToFolder(
bucketName,
filePath,
file
);
uploadData = data;
if (uploadError) {
errorMessages.push(`Error uploading file: ${uploadError.message}`);
}
}
if (errorMessages.length > 0) {
Swal.fire({
icon: "error",
title: "Errors occurred",
html: errorMessages.join("<br>"),
confirmButtonColor: "red",
});
return { success: false, errors: errorMessages, data: null };
}
return { success: true, errors: null, data: uploadData };
}

13
src/app/api/userApi.ts Normal file
View File

@ -0,0 +1,13 @@
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
export async function getCurrentUserID() {
const supabase = createSupabaseClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
console.error('Error fetching user:', error);
return;
}
return user.id;
}

View File

@ -1,834 +1,185 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { useEffect, useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { useForm } from "react-hook-form";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useState, useEffect, useRef } from "react";
import { SubmitHandler } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { DualOptionSelector } from "@/components/dualSelector";
import { MultipleOptionSelector } from "@/components/multipleSelector";
import BusinessForm from "@/components/BusinessForm";
import { businessFormSchema } from "@/types/schemas/application.schema";
import Swal from "sweetalert2";
import { getCurrentUserID } from "@/app/api/userApi";
import { uploadFile } from "@/app/api/generalApi";
import { Loader } from "@/components/loading/loader";
export default function Apply() {
const [industry, setIndustry] = useState<string[]>([]);
const [isInUS, setIsInUS] = useState("");
const [isForSale, setIsForSale] = useState("");
const [isGenerating, setIsGenerating] = useState("");
const [businessPitch, setBusinessPitch] = useState("text");
const [projectType, setProjectType] = useState<string[]>([]);
const [projectPitch, setProjectPitch] = useState("text");
type businessSchema = z.infer<typeof businessFormSchema>;
const BUCKET_PITCH_NAME = "business-application";
let supabase = createSupabaseClient();
export default function ApplyBusiness() {
const [applyProject, setApplyProject] = useState(false);
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [businessPitchFile, setBusinessPitchFile] = useState("");
const [projectPitchFile, setProjectPitchFile] = useState("");
const MAX_FILE_SIZE = 5000000;
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
const createPitchDeckSchema = (inputType: string) => {
if (inputType === "text") {
return z
.string()
.url("Pitch deck must be a valid URL.")
.refine((url) => url.endsWith(".md"), {
message: "Pitch deck URL must link to a markdown file (.md).",
});
} else if (inputType === "file") {
return z
.custom<File>(
(val) => {
// confirm val is a File object
return val instanceof File; // Ensure it is a File instance
},
{
message: "Input must be a file.",
}
)
.refine((file) => file.size < MAX_FILE_SIZE, {
message: "File can't be bigger than 5MB.",
})
.refine((file) => file.name.endsWith(".md"), {
message: "File must be a markdown file (.md).",
});
} else {
return z.any(); // avoid undefined
}
const alertShownRef = useRef(false);
const [success, setSucess] = useState(false);
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
const transformedData = await transformChoice(data);
await sendApplication(transformedData);
};
const imageSchema = 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.",
});
const sendApplication = async (recvData: any) => {
setSucess(false);
const {
data: { user },
} = await supabase.auth.getUser();
const pitchType = typeof recvData["businessPitchDeck"];
if (pitchType === "object") {
if (user?.id) {
const uploadSuccess = await uploadFile(
recvData["businessPitchDeck"],
BUCKET_PITCH_NAME,
// file structure: userId/fileName
`${user?.id}/pitch-file/pitch.md`
);
const projectFormSchema = z.object({
projectName: z.string().min(5, {
message: "Project name must be at least 5 characters.",
}),
projectType: z.string({
required_error: "Please select one of the option",
}),
shortDescription: z
.string({
required_error: "Please provide a brief description for your project",
})
.min(10, {
message: "Short description must be at least 10 characters.",
}),
projectPitchDeck: createPitchDeckSchema(projectPitch),
projectLogo: imageSchema,
projectPhotos: z.custom(
(value) => {
console.log("Tozod", value);
if (value instanceof FileList || Array.isArray(value)) {
if (value.length === 1) {
return false;
}
return Array.from(value).every((item) => item instanceof File);
if (!uploadSuccess) {
return;
}
return false;
},
{ message: "Must be a FileList or an array of File objects with at least one file." }
),
minInvest: z
.number({
required_error: "Minimum invesment must be a number.",
invalid_type_error: "Minimum invesment must be a valid number.",
})
.positive()
.max(9999999999, "Minimum invesment must be a realistic amount."),
targetInvest: z
.number({
required_error: "Target invesment must be a number.",
invalid_type_error: "Target invesment must be a valid number.",
})
.positive()
.max(9999999999, "Target invesment must be a realistic amount."),
deadline: z
.string()
.min(1, "Deadline is required.")
.refine((value) => !isNaN(Date.parse(value)), {
message: "Invalid date-time format.",
})
.transform((value) => new Date(value))
.refine((date) => date > new Date(), {
message: "Deadline must be in the future.",
}),
});
const businessFormSchema = z.object({
companyName: z.string().min(5, {
message: "Company name must be at least 5 characters.",
}),
industry: z.string({
required_error: "Please select one of the option",
}),
isInUS: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
isForSale: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
isGenerating: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
totalRaised: z
.number({
required_error: "Total raised must be a number.",
invalid_type_error: "Total raised must be a valid number.",
})
.positive()
.max(9999999999, "Total raised must be a realistic amount."),
communitySize: z.string({
required_error: "Please select one of the option",
}),
businessPitchDeck: createPitchDeckSchema(businessPitch),
});
let supabase = createSupabaseClient();
const {
register,
handleSubmit,
setValue: setValueBusiness,
formState: { errors: errorsBusiness },
} = useForm({
resolver: zodResolver(businessFormSchema),
});
const {
register: registerSecondForm,
handleSubmit: handleSecondSubmit,
formState: { errors: errorsProject },
setValue: setValueProject,
} = useForm({
resolver: zodResolver(projectFormSchema),
});
const communitySize = ["N/A", "0-5K", "5-10K", "10-20K", "20-50K", "50-100K", "100K+"];
useEffect(() => {
register("industry");
register("isInUS");
register("isForSale");
register("isGenerating");
}, [register]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const filesArray = Array.from(event.target.files);
console.log("first file", filesArray);
setSelectedImages((prevImages) => {
const updatedImages = [...prevImages, ...filesArray];
console.log("Updated Images Array:", updatedImages);
// ensure we're setting an array of File objects
setValueProject("projectPhotos", updatedImages);
return updatedImages;
});
console.log("file upload successful");
} else {
console.error("user ID is undefined.");
return;
}
}
};
const handleRemoveImage = (index: number) => {
setSelectedImages((prevImages) => {
const updatedImages = prevImages.filter((_, i) => i !== index);
console.log("After removal - Updated Images:", updatedImages);
// ensure we're setting an array of File objects
setValueProject("projectPhotos", updatedImages);
return updatedImages;
const { data, error } = await supabase
.from("business_application")
.insert([
{
user_id: user?.id,
business_name: recvData["companyName"],
business_type_id: recvData["industry"],
location: recvData["country"],
is_for_sale: recvData["isForSale"],
is_generating_revenue: recvData["isGenerating"],
is_in_us: recvData["isInUS"],
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,
confirmButtonColor: error == null ? "green" : "red",
}).then((result) => {
if (result.isConfirmed && applyProject) {
window.location.href = "/project/apply";
} else {
window.location.href = "/";
}
});
};
const ensureArrayValue = (value: any): File[] => {
if (Array.isArray(value)) return value;
if (value instanceof File) return [value];
return [];
const hasUserApplied = async (userID: string) => {
let { data: business, error } = await supabase
.from("business")
.select("*")
.eq("user_id", userID);
console.table(business);
if (error) {
console.error(error);
}
if (business) {
return true;
}
return false;
};
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;
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
}
} else {
acc[key] = value; // keep other string values unchanged
acc[key] = value; // keep other types unchanged
}
} else {
acc[key] = value; // keep other types unchanged
}
return acc;
}, {});
return acc;
},
{}
);
return transformedData;
};
const handleBusinessPitchChange = (type: string) => {
setBusinessPitch(type);
// clear out old data
setValueBusiness("pitchDeck", "");
};
const handleBusinessFieldChange = (fieldName: string, value: any) => {
switch (fieldName) {
case "isInUS":
setIsInUS(value);
break;
case "isForSale":
setIsForSale(value);
break;
case "isGenerating":
setIsGenerating(value);
break;
}
setValueBusiness(fieldName, value);
};
const handleProjectFieldChange = (fieldName: string, value: any) => {
switch (fieldName) {
}
setValueProject(fieldName, value);
};
const fetchIndustry = async () => {
let { data: BusinessType, error } = await supabase.from("business_type").select("value");
if (error) {
console.error(error);
} else {
if (BusinessType) {
// console.table();
setIndustry(BusinessType.map((item) => item.value));
}
}
};
const onSubmitSingleForm = (data: any) => {
const pitchDeckSchema = createPitchDeckSchema(businessPitch);
pitchDeckSchema.parse(data.businessPitchDeck);
console.log("Valid form input:", data);
alert(JSON.stringify(data));
};
const onSubmitBothForms = (firstFormData: any, secondFormData: any) => {
const formattedSecondFormData = {
...secondFormData,
projectPhotos: ensureArrayValue(secondFormData.projectPhotos),
};
alert(JSON.stringify(firstFormData));
alert(JSON.stringify(formattedSecondFormData));
console.log("Both forms submitted:", {
firstFormData,
formattedSecondFormData,
});
};
const handleSubmitForms = (firstFormData: any) => {
const transformedData = transformChoice(firstFormData);
if (applyProject) {
handleSecondSubmit((secondFormData: any) => {
onSubmitBothForms(transformedData, secondFormData);
})();
} else {
onSubmitSingleForm(transformedData);
}
};
const fetchProjectType = async () => {
let { data: ProjectType, error } = await supabase.from("project_type").select("value");
if (error) {
console.error(error);
} else {
if (ProjectType) {
console.table(ProjectType);
setProjectType(ProjectType.map((item) => item.value));
}
}
};
useEffect(() => {
fetchIndustry();
fetchProjectType();
const fetchUserData = async () => {
try {
setSucess(false);
const userID = await getCurrentUserID();
if (userID) {
const hasApplied = await hasUserApplied(userID);
setSucess(true);
if (hasApplied && !alertShownRef.current) {
alertShownRef.current = true;
Swal.fire({
icon: "info",
title: "You Already Have an Account",
text: "You have already submitted your business application.",
confirmButtonText: "OK",
allowOutsideClick: false,
allowEscapeKey: false,
}).then((result) => {
if (result.isConfirmed) {
window.location.href = "/";
}
});
}
setSucess(false);
} else {
console.error("User ID is undefined.");
}
} catch (error) {
console.error("Error fetching user ID:", error);
}
};
// setSucess(true);
fetchUserData();
}, []);
return (
<div>
<Loader isSuccess={success} />
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-100 dark:bg-gray-800 p-5">
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
Apply to raise on B2DVentures
</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)}>
<div className="grid grid-flow-row auto-rows-max w-3/4 ml-1/2 lg:ml-[10%]">
<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.
</p>
{/* company name */}
<div className="ml-96 mt-5 space-y-10">
<div className="mt-10 space-y-5">
<Label htmlFor="companyName" className="font-bold text-lg">
Company name
</Label>
<div className="flex space-x-5">
<Input type="text" id="companyName" className="w-96" {...register("companyName")} />
<span className="text-[12px] text-neutral-500 self-center">
This should be the name your company uses on your <br />
website and in the market.
</span>
</div>
{errorsBusiness.companyName && (
<p className="text-red-500 text-sm">
{errorsBusiness.companyName && (
<p className="text-red-500 text-sm">{errorsBusiness.companyName.message as string}</p>
)}
</p>
)}
</div>
{/* industry */}
<MultipleOptionSelector
header={<>Industry</>}
fieldName="industry"
choices={industry}
handleFunction={handleBusinessFieldChange}
description={<>Choose the industry that best aligns with your business.</>}
placeholder="Select an industry"
selectLabel="Industry"
/>
{errorsBusiness.industry && (
<p className="text-red-500 text-sm">{errorsBusiness.industry.message as string}</p>
)}
{/* How much money has your company raised to date? */}
<div className="space-y-5">
<Label htmlFor="totalRaised" className="font-bold text-lg">
How much money has your company <br /> raised to date?
</Label>
<div className="flex space-x-5">
<Input
type="number"
id="totalRaised"
className="w-96"
placeholder="$ 1,000,000"
{...register("totalRaised", {
valueAsNumber: true,
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
The sum total of past financing, including angel or venture <br />
capital, loans, grants, or token sales.
</span>
</div>
{errorsBusiness.totalRaised && (
<p className="text-red-500 text-sm">{errorsBusiness.totalRaised.message as string}</p>
)}
</div>
{/* Is your company incorporated in the United States? */}
<DualOptionSelector
label={
<>
Is your company incorporated in the <br />
United States?
</>
}
name="isInUS"
choice1="Yes"
choice2="No"
handleFunction={handleBusinessFieldChange}
description={
<>
Only companies that are incorporated or formed in the US are <br />
eligible to raise via Reg CF. If your company is incorporated <br />
outside the US, we still encourage you to apply.
</>
}
value={isInUS}
/>
{errorsBusiness.isInUS && <p className="text-red-500 text-sm">{errorsBusiness.isInUS.message as string}</p>}
{/* Is your product available (for sale) in market? */}
<DualOptionSelector
label={
<>
Is your product available (for sale) <br />
in market?
</>
}
name="isForSale"
choice1="Yes"
choice2="No"
handleFunction={handleBusinessFieldChange}
description={
<>
Only check this box if customers can access, use, or buy your <br />
product today.
</>
}
value={isForSale}
/>
{errorsBusiness.isForSale && (
<p className="text-red-500 text-sm">{errorsBusiness.isForSale.message as string}</p>
)}
{/* Is your company generating revenue?*/}
<DualOptionSelector
label={<>Is your company generating revenue?</>}
name="isGenerating"
choice1="Yes"
choice2="No"
handleFunction={handleBusinessFieldChange}
description={
<>
Only check this box if your company is making money. <br />
Please elaborate on revenue and other traction below.
</>
}
value={isGenerating}
/>
{errorsBusiness.isGenerating && (
<p className="text-red-500 text-sm">{errorsBusiness.isGenerating.message as string}</p>
)}
{/* Pitch deck */}
<div className="space-y-5">
<Label htmlFor="pitchDeck" className="font-bold text-lg">
Pitch deck
</Label>
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={businessPitch === "text" ? "default" : "outline"}
onClick={() => handleBusinessPitchChange("text")}
className="w-32 h-12 text-base">
Paste URL
</Button>
<Button
type="button"
variant={businessPitch === "file" ? "default" : "outline"}
onClick={() => handleBusinessPitchChange("file")}
className="w-32 h-12 text-base">
Upload a file
</Button>
</div>
<div className="flex space-x-5">
<Input
type={businessPitch === "file" ? "file" : "text"}
id="pitchDeck"
className="w-96"
placeholder={businessPitch === "file" ? "Upload your Markdown file" : "https:// "}
accept={businessPitch === "file" ? ".md" : undefined}
// if text use normal register
{...(businessPitch === "text"
? register("businessPitchDeck", { required: true })
: {
onChange: (e) => {
const file = e.target.files?.[0];
setValueBusiness("businessPitchDeck", file);
setBusinessPitchFile(file?.name || "");
},
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
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.
<br />
<p className="text-red-500">** support only markdown(.md) format</p>
</span>
</div>
{/* box to show file name */}
{businessPitchFile && (
<div className="flex justify-between items-center border p-2 rounded w-96 text-sm text-foreground">
<span>1. {businessPitchFile}</span>
<Button
className="ml-4"
onClick={() => {
setValueBusiness("businessPitchDeck", null);
setBusinessPitchFile("");
}}>
Remove
</Button>
</div>
)}
</div>
{errorsBusiness.businessPitchDeck && (
<p className="text-red-500 text-sm">{errorsBusiness.businessPitchDeck.message as string}</p>
)}
<MultipleOptionSelector
header={
<>
What's the rough size of your <br /> community?
</>
}
fieldName="communitySize"
choices={communitySize}
handleFunction={handleBusinessFieldChange}
description={
<>
{" "}
Include your email list, social media following (i.e. Instagram, <br /> Discord, Facebook, Twitter,
TikTok). Wed like to understand the <br /> rough size of your current audience.
</>
}
placeholder="Select"
selectLabel="Select"
/>
{errorsBusiness.communitySize && (
<p className="text-red-500 text-sm">{errorsBusiness.communitySize.message as string}</p>
)}
<div className="flex space-x-5">
<Switch onCheckedChange={() => setApplyProject(!applyProject)}></Switch>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-[12px] text-neutral-500 self-center cursor-pointer">
Would you like to apply for your first fundraising project as well?
</span>
</TooltipTrigger>
<TooltipContent>
<p className="text-[11px]">
Toggling this option allows you to begin your first project, <br /> which is crucial for unlocking
the tools necessary to raise funds.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
{/* apply first project */}
{applyProject && (
<div className="grid auto-rows-max w-3/4 ml-48 bg-zinc-100 dark:bg-zinc-900 mt-10 pt-12 pb-12">
{/* header */}
<div className="ml-[15%]">
<h1 className="text-3xl font-bold mt-10">Begin Your First Fundraising Project</h1>
<p className="mt-3 text-sm text-neutral-500">
Starting a fundraising project is mandatory for all businesses. This step is crucial <br />
to begin your journey and unlock the necessary tools for raising funds.
</p>
{/* project's name */}
<div className="mt-10 space-y-5">
<Label htmlFor="projectName" className="font-bold text-lg">
Project name
</Label>
<div className="flex space-x-5">
<Input type="text" id="projectName" className="w-96" {...registerSecondForm("projectName")} />
</div>
</div>
{errorsProject.projectName && (
<p className="text-red-500 text-sm">{errorsProject.projectName.message as string}</p>
)}
{/* project type */}
<MultipleOptionSelector
header={<>Project type</>}
fieldName="projectType"
choices={projectType}
handleFunction={handleProjectFieldChange}
description={<>Please specify the primary purpose of the funds</>}
placeholder="Select a Project type"
selectLabel="Project type"
/>
{errorsProject.projectType && (
<p className="text-red-500 text-sm">{errorsProject.projectType.message as string}</p>
)}
{/* short description */}
<div className="mt-10 space-y-5">
<Label htmlFor="shortDescription" className="font-bold text-lg">
Short description
</Label>
<div className="flex space-x-5">
<Textarea id="shortDescription" className="w-96" {...registerSecondForm("shortDescription")} />
<span className="text-[12px] text-neutral-500 self-center">
Could you provide a brief description of your project <br /> in one or two sentences?
</span>
</div>
</div>
{errorsProject.shortDescription && (
<p className="text-red-500 text-sm">{errorsProject.shortDescription.message as string}</p>
)}
{/* Pitch deck */}
<div className="mt-10 space-y-5">
<Label htmlFor="projectPitchDeck" className="font-bold text-lg">
Pitch deck
</Label>
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={projectPitch === "text" ? "default" : "outline"}
onClick={() => setProjectPitch("text")}
className="w-32 h-12 text-base">
Paste URL
</Button>
<Button
type="button"
variant={projectPitch === "file" ? "default" : "outline"}
onClick={() => setProjectPitch("file")}
className="w-32 h-12 text-base">
Upload a file
</Button>
</div>
<div className="flex space-x-5">
<Input
type={projectPitch}
id="projectPitchDeck"
className="w-96"
placeholder={projectPitch === "file" ? "Upload your Markdown file" : "https:// "}
accept={projectPitch === "file" ? ".md" : undefined}
{...(projectPitch === "text"
? registerSecondForm("projectPitchDeck", {
required: true,
})
: {
onChange: (e) => {
const file = e.target.files?.[0];
setValueProject("projectPitchDeck", file);
setProjectPitchFile(file?.name || "");
},
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload a file or paste a link to your pitch, which should <br />
cover key aspects of your project: what it will do, what investors <br /> can expect to gain, and
any highlights that make it stand out.
</span>
</div>
{projectPitchFile && (
<div className="flex justify-between items-center border p-2 rounded w-96 text-sm text-foreground">
<span>1. {projectPitchFile}</span>
<Button
className="ml-4"
onClick={() => {
setValueProject("projectPitchDeck", "");
setProjectPitchFile("");
}}>
Remove
</Button>
</div>
)}
</div>
{errorsProject.projectPitchDeck && (
<p className="text-red-500 text-sm">{errorsProject.projectPitchDeck.message as string}</p>
)}
{/* project logo */}
<div className="mt-10 space-y-5">
<Label htmlFor="projectLogo" className="font-bold text-lg mt-10">
Project logo
</Label>
<div className="flex space-x-5">
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
registerSecondForm("projectLogo").onChange({
target: { name: "projectLogo", value: file },
});
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your project.
</span>
</div>
</div>
{errorsProject.projectLogo && (
<p className="text-red-500 text-sm">{errorsProject.projectLogo.message as string}</p>
)}
<div className="mt-10 space-y-5">
<Label htmlFor="projectPhotos" className="font-bold text-lg mt-10">
Project photos
</Label>
<div className="flex space-x-5">
<Input
type="file"
id="projectPhotos"
multiple
accept="image/*"
className="w-96"
{...registerSecondForm("projectPhotos", {
required: true,
onChange: handleFileChange,
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
Feel free to upload any additional images that provide <br />
further insight into your project.
</span>
</div>
<div className="mt-5 space-y-2 w-96">
{selectedImages.map((image, index) => (
<div key={index} className="flex justify-between items-center border p-2 rounded">
<span>{image.name}</span>
<Button variant="outline" onClick={() => handleRemoveImage(index)} className="ml-4" type="reset">
Remove
</Button>
</div>
))}
</div>
</div>
{errorsProject.projectPhotos && (
<p className="text-red-500 text-sm">{errorsProject.projectPhotos.message as string}</p>
)}
{/* Minimum Investment */}
<div className="space-y-5 mt-10">
<Label htmlFor="minInvest" className="font-bold text-lg">
Minimum investment
</Label>
<div className="flex space-x-5">
<Input
type="number"
id="minInvest"
className="w-96"
placeholder="$ 500"
{...registerSecondForm("minInvest", {
valueAsNumber: true,
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
This helps set clear expectations for investors
</span>
</div>
</div>
{errorsProject.minInvest && (
<p className="text-red-500 text-sm">{errorsProject.minInvest.message as string}</p>
)}
{/* Target Investment */}
<div className="space-y-5 mt-10">
<Label htmlFor="targetInvest" className="font-bold text-lg">
Target investment
</Label>
<div className="flex space-x-5">
<Input
type="number"
id="targetInvest"
className="w-96"
placeholder="$ 1,000,000"
{...registerSecondForm("targetInvest", {
valueAsNumber: true,
})}
/>
<span className="text-[12px] text-neutral-500 self-center">
We encourage you to set a specific target investment <br /> amount that reflects your funding goals.
</span>
</div>
</div>
{errorsProject.targetInvest && (
<p className="text-red-500 text-sm">{errorsProject.targetInvest.message as string}</p>
)}
{/* Deadline */}
<div className="space-y-5 mt-10">
<Label htmlFor="deadline" className="font-bold text-lg">
Deadline
</Label>
<div className="flex space-x-5">
<Input type="datetime-local" id="deadline" className="w-96" {...registerSecondForm("deadline")} />
<span className="text-[12px] text-neutral-500 self-center">
What is the deadline for your fundraising project? Setting <br /> a clear timeline can help motivate
potential investors.
</span>
</div>
</div>
{errorsProject.deadline && (
<p className="text-red-500 text-sm">{errorsProject.deadline.message as string}</p>
)}
</div>
</div>
)}
{/* Submit */}
<center>
<Button className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5" type="submit">
Submit application
</Button>
</center>
</form>
{/* <form action="" onSubmit={handleSubmit(handleSubmitForms)}> */}
<BusinessForm
onSubmit={onSubmit}
applyProject={applyProject}
setApplyProject={setApplyProject}
/>
</div>
);
}
}

17
src/app/dashboard/hook.ts Normal file
View File

@ -0,0 +1,17 @@
import { useEffect, useState } from "react";
import { Deal, getDealList } from "../api/dealApi";
// custom hook for deal list
export function useDealList() {
const [dealList, setDealList] = useState<Deal[]>();
const fetchDealList = async () => {
setDealList(await getDealList());
}
useEffect(() => {
fetchDealList();
}, []);
return dealList;
}

View File

@ -12,10 +12,22 @@ import { Overview } from "@/components/ui/overview";
import { RecentFunds } from "@/components/recent-funds";
import { useState } from "react";
import { useDealList } from "./hook";
export default function Dashboard() {
const [graphType, setGraphType] = useState("line");
const dealList = useDealList();
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
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"
@ -35,7 +47,7 @@ export default function Dashboard() {
<div className="hidden flex-col md:flex">
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
@ -63,10 +75,10 @@ export default function Dashboard() {
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
<div className="text-2xl font-bold">${totalDealAmount}</div>
{/* <p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</p> */}
</CardContent>
</Card>
<Card>
@ -90,9 +102,9 @@ export default function Dashboard() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">
{/* <p className="text-xs text-muted-foreground">
+180.1% from last month
</p>
</p> */}
</CardContent>
</Card>
<Card>
@ -117,9 +129,9 @@ export default function Dashboard() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">
{/* <p className="text-xs text-muted-foreground">
+19% from last month
</p>
</p> */}
</CardContent>
</Card>
{/* <Card>
@ -181,11 +193,12 @@ export default function Dashboard() {
<CardHeader>
<CardTitle>Recent Funds</CardTitle>
<CardDescription>
You made 265 sales this month.
You made {dealList?.length || 0} sales this month.
</CardDescription>
</CardHeader>
<CardContent>
<RecentFunds />
<RecentFunds>
</RecentFunds>
</CardContent>
</Card>
</div>

View File

@ -1,39 +1,63 @@
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";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { Suspense } from "react";
import { FC } from "react";
const TopProjects = async () => {
const supabase = createSupabaseClient();
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
interface Project {
id: number;
project_name: string;
project_short_description: string;
card_image_url: string;
published_time: string;
business: { location: string }[];
project_tag: { tag: { id: number; value: string }[] }[];
project_investment_detail: {
min_investment: number;
total_investment: number;
}[];
}
if (topProjectsError) {
return <div>Error loading top projects: {topProjectsError}</div>;
}
interface TopProjectsProps {
projects: Project[];
}
if (!topProjectsData || topProjectsData.length === 0) {
const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
if (!projects || projects.length === 0) {
return <div>No top projects available.</div>;
}
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{topProjectsData.map((project) => (
{projects.map((project) => (
<Link href={`/deals/${project.id}`} key={project.id}>
<ProjectCard
name={project.project_name}
description={project.project_short_description}
imageUri={project.card_image_url}
joinDate={new Date(project.published_time).toLocaleDateString()}
location={project.business.location}
tags={project.item_tag.map((item) => item.tag.value)}
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
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) : []
)}
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>
))}
@ -44,12 +68,18 @@ const TopProjects = async () => {
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);
return (
<main>
<div className="relative mx-auto">
@ -57,9 +87,14 @@ 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>
@ -107,11 +142,23 @@ 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>
@ -123,10 +170,12 @@ 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 />
<TopProjects projects={topProjectsData || []} />
</Suspense>
<div className="self-center py-5 scale-75 md:scale-100">
<Button>

View File

@ -0,0 +1,282 @@
"use client";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import ProjectForm from "@/components/ProjectForm";
import { projectFormSchema } from "@/types/schemas/application.schema";
import { z } from "zod";
import { SubmitHandler } from "react-hook-form";
import Swal from "sweetalert2";
import { uploadFile } from "@/app/api/generalApi";
import { Loader } from "@/components/loading/loader";
import { useState } from "react";
import { errors } from "@playwright/test";
type projectSchema = z.infer<typeof projectFormSchema>;
let supabase = createSupabaseClient();
const BUCKET_PITCH_APPLICATION_NAME = "project-application";
export default function ApplyProject() {
const [isSuccess, setIsSuccess] = useState(true);
const onSubmit: SubmitHandler<projectSchema> = async (data) => {
alert("มาแน้ววว");
await sendApplication(data);
// console.table(data);
// console.log(typeof data["projectPhotos"], data["projectPhotos"]);
};
const saveApplicationData = async (recvData: any, userId: string) => {
const pitchType = typeof recvData["projectPitchDeck"];
const { data: projectData, error: projectError } = await supabase
.from("project_application")
.insert([
{
user_id: userId,
pitch_deck_url:
pitchType === "string" ? recvData["projectPitchDeck"] : "",
target_investment: recvData["targetInvest"],
deadline: recvData["deadline"],
project_name: recvData["projectName"],
project_type_id: recvData["projectType"],
short_description: recvData["shortDescription"],
min_investment: recvData["minInvest"],
},
])
.select();
return { projectId: projectData?.[0]?.id, error: projectError };
};
const saveTags = async (tags: string[], projectId: string) => {
const tagPromises = tags.map(async (tag) => {
const response = await supabase
.from("project_application_tag")
.insert([{ tag_id: tag, item_id: projectId }])
.select();
// console.log("Insert response for tag:", tag, response);
return response;
});
const results = await Promise.all(tagPromises);
// Collect errors
const errors = results
.filter((result) => result.error)
.map((result) => result.error);
return { errors };
};
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}`
);
};
const uploadLogoAndPhotos = async (
logoFile: File,
photos: File[],
userId: string,
projectId: string
) => {
const uploadResults: { logo?: any; photos: any[] } = { photos: [] };
// upload logo
if (logoFile) {
const logoResult = await uploadFile(
logoFile,
BUCKET_PITCH_APPLICATION_NAME,
`${userId}/${projectId}/logo/${logoFile.name}`
);
if (!logoResult.success) {
console.error("Error uploading logo:", logoResult.errors);
return { success: false, logo: logoResult, photos: [] };
}
uploadResults.logo = logoResult;
}
// upload each photo
const uploadPhotoPromises = photos.map((image) =>
uploadFile(
image,
BUCKET_PITCH_APPLICATION_NAME,
`${userId}/${projectId}/photos/${image.name}`
)
);
const photoResults = await Promise.all(uploadPhotoPromises);
uploadResults.photos = photoResults;
// check if all uploads were successful
const allUploadsSuccessful = photoResults.every((result) => result.success);
return {
success: allUploadsSuccessful,
logo: uploadResults.logo,
photos: uploadResults.photos,
};
};
const displayAlert = (error: any) => {
Swal.fire({
icon: error == null ? "success" : "error",
title: error == null ? "Success" : `Error: ${error.code}`,
text:
error == null ? "Your application has been submitted" : error.message,
confirmButtonColor: error == null ? "green" : "red",
}).then((result) => {
if (result.isConfirmed) {
// window.location.href = "/";
}
});
};
const sendApplication = async (recvData: any) => {
setIsSuccess(false);
const {
data: { user },
} = await supabase.auth.getUser();
if (!user?.id) {
console.error("User ID is undefined.");
return;
}
// save application data
const { projectId, error } = await saveApplicationData(recvData, user.id);
if (error) {
displayAlert(error);
return;
}
const tagError = await saveTags(recvData["tag"], projectId);
// if (tagError) {
// displayAlert(tagError);
// return;
// }
// upload pitch file if its a file
if (typeof recvData["projectPitchDeck"] === "object") {
const uploadPitchSuccess = await uploadPitchFile(
recvData["projectPitchDeck"],
user.id,
projectId
);
if (!uploadPitchSuccess) {
console.error("Error uploading pitch file.");
} else {
console.log("Pitch file uploaded successfully.");
}
}
// upload logo and photos
const { success, logo, photos } = await uploadLogoAndPhotos(
recvData["projectLogo"],
recvData["projectPhotos"],
user.id,
projectId
);
if (!success) {
console.error("Error uploading media files.");
}
// console.log("Bucket Name:", BUCKET_PITCH_APPLICATION_NAME);
// console.log("Logo Path:", logo.data.path);
// console.table(photos);
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
);
if (photoURL?.signedUrl) {
photoURLsArray.push(photoURL.signedUrl);
} else {
console.error("Signed URL for photo is undefined.");
}
}
);
await Promise.all(photoURLPromises);
// console.log(logoURL.publicUrl, projectId, logo.data.path);
// console.log(logoURL?.signedUrl, projectId);
// console.log(photoURLsArray[0], photoURLsArray[1]);
if (logoURL?.signedUrl) {
await updateImageURL(logoURL.signedUrl, "project_logo", projectId);
} else {
console.error("Signed URL for logo is undefined.");
}
await updateImageURL(photoURLsArray, "project_photos", projectId);
// console.log(logoURL, photosUrl);
setIsSuccess(true);
displayAlert(error);
};
const updateImageURL = async (
url: string | string[],
columnName: string,
projectId: number
) => {
const { error } = await supabase
.from("project_application")
.update({ [columnName]: url })
.eq("id", projectId);
// console.log(
// `Updating ${columnName} with URL: ${url} for project ID: ${projectId}`
// );
if (error) {
console.error(error);
}
};
const getPrivateURL = async (path: string, bucketName: string) => {
const { data } = await supabase.storage
.from(bucketName)
.createSignedUrl(path, 9999999999999999999999999999);
// console.table(data);
return data;
};
return (
<div>
<Loader isSuccess={isSuccess} />
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-2s00 dark:bg-gray-800 p-5">
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
Apply to raise on B2DVentures
</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.
</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.
</p>
</div>
</div>
<div className="grid auto-rows-max bg-zinc-100 dark:bg-zinc-900 pt-12 -mb-6">
<ProjectForm onSubmit={onSubmit} />
</div>
</div>
);
}

View File

@ -0,0 +1,501 @@
import { useEffect, useState } from "react";
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 { 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 { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
type businessSchema = z.infer<typeof businessFormSchema>;
interface BusinessFormProps {
applyProject: boolean;
setApplyProject: Function;
onSubmit: SubmitHandler<businessSchema>;
}
const BusinessForm = ({
applyProject,
setApplyProject,
onSubmit,
}: BusinessFormProps & { onSubmit: SubmitHandler<businessSchema> }) => {
const communitySize = [
{ id: 1, name: "N/A" },
{ id: 2, name: "0-5K" },
{ id: 3, name: "5-10K" },
{ id: 4, name: "10-20K" },
{ id: 5, name: "20-50K" },
{ id: 6, name: "50-100K" },
{ id: 7, name: "100K+" },
];
const form = useForm<businessSchema>({
resolver: zodResolver(businessFormSchema),
defaultValues: {},
});
let supabase = createSupabaseClient();
const [businessPitch, setBusinessPitch] = useState("text");
const [businessPitchFile, setBusinessPitchFile] = useState("");
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");
if (error) {
console.error(error);
} else {
if (BusinessType) {
// console.table();
setIndustry(
BusinessType.map((item) => ({
id: item.id,
name: item.value,
}))
);
}
}
};
const fetchCountries = async () => {
try {
const response = await fetch("https://restcountries.com/v3.1/all");
if (!response.ok) {
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,
})
);
setCountries(
countryList.sort((a: { name: string }, b: { name: any }) =>
a.name.localeCompare(b.name)
)
);
} catch (error) {
console.error("Error fetching countries:", error);
}
};
useEffect(() => {
fetchCountries();
fetchIndustry();
}, []);
return (
<Form {...form}>
<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.
</p>
<div className="ml-96 mt-5 space-y-10">
{/* Company Name */}
<FormField
control={form.control}
name="companyName"
render={({ field }: { field: any }) => (
<FormItem>
<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}
/>
<span className="text-[12px] text-neutral-500 self-center">
This should be the name your company uses on your{" "}
<br />
website and in the market.
</span>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Country */}
<FormField
control={form.control}
name="country"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<MultipleOptionSelector
header={<>Country</>}
fieldName="country"
choices={countries}
handleFunction={(selectedValues: any) => {
// console.log("Country selected: " + selectedValues.name);
field.onChange(selectedValues.name);
}}
description={
<>Select the country where your business is based.</>
}
placeholder="Select a country"
selectLabel="Country"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Industry */}
<FormField
control={form.control}
name="industry"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<MultipleOptionSelector
header={<>Industry</>}
fieldName="industry"
choices={industry}
handleFunction={(selectedValues: any) => {
// console.log("Type of selected value:", selectedValues.id);
field.onChange(selectedValues.id);
}}
description={
<>
Choose the industry that best aligns with your
business.
</>
}
placeholder="Select an industry"
selectLabel="Industry"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Raised Money */}
<FormField
control={form.control}
name="totalRaised"
render={({ field }) => (
<FormItem>
<div className="mt-10 space-y-5">
<Label htmlFor="totalRaised" className="font-bold text-lg">
How much money has your company <br /> raised to date?
</Label>
<FormControl>
<div className="flex space-x-5">
<Input
type="number"
id="totalRaised"
className="w-96"
placeholder="$ 1,000,000"
{...field}
onChange={(e) => {
const value = e.target.value;
field.onChange(value ? parseFloat(value) : null);
}}
value={field.value}
/>
<span className="text-[12px] text-neutral-500 self-center">
The sum total of past financing, including angel or
venture <br />
capital, loans, grants, or token sales.
</span>
</div>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
{/* Incorporated in US */}
<FormField
control={form.control}
name="isInUS"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex space-x-5">
<DualOptionSelector
name="isInUS"
label={
<>
Is your company incorporated in the United States?
</>
}
choice1="Yes"
choice2="No"
handleFunction={(selectedValues: string) => {
// setIsInUS;
field.onChange(selectedValues);
}}
description={<></>}
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.
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Product for Sale */}
<FormField
control={form.control}
name="isForSale"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex space-x-5">
<DualOptionSelector
name="isForSale"
value={field.value}
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.
</>
}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Generating Revenue */}
<FormField
control={form.control}
name="isGenerating"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex space-x-5">
<DualOptionSelector
name="isGenerating"
label={<>Is your company generating revenue?</>}
choice1="Yes"
choice2="No"
value={field.value}
handleFunction={(selectedValues: string) => {
field.onChange(selectedValues);
}}
description={
<>
Only check this box if your company is making money.
Please elaborate on revenue below.
</>
}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Pitch Deck */}
<FormField
control={form.control}
name="businessPitchDeck"
render={({ field }) => (
<FormItem>
<div className="space-y-5 mt-10">
<Label htmlFor="pitchDeck" className="font-bold text-lg">
Pitch deck
</Label>
<FormControl>
<div>
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={
businessPitch === "text" ? "default" : "outline"
}
onClick={() => setBusinessPitch("text")}
className="w-32 h-12 text-base"
>
Paste URL
</Button>
<Button
type="button"
variant={
businessPitch === "file" ? "default" : "outline"
}
onClick={() => setBusinessPitch("file")}
className="w-32 h-12 text-base"
>
Upload a file
</Button>
</div>
<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
}
onChange={(e) => {
const value = e.target;
if (businessPitch === "file") {
const file = value.files?.[0];
field.onChange(file || "");
} else {
field.onChange(value.value);
}
}}
className="w-96 mt-5"
/>
<span className="text-[12px] text-neutral-500 self-center">
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.
<br />
<p className="text-red-500">
** support only markdown(.md) format
</p>
</span>
</div>
{businessPitchFile && (
<div className="flex justify-between items-center border p-2 rounded w-96 text-sm text-foreground">
<span>1. {businessPitchFile}</span>
<Button
className="ml-4"
onClick={() => {
setBusinessPitchFile("");
}}
>
Remove
</Button>
</div>
)}
</div>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
{/* Community Size */}
<FormField
control={form.control}
name="communitySize"
render={({ field }) => (
<FormItem>
<FormControl>
<MultipleOptionSelector
header={<>What's the rough size of your community?</>}
fieldName="communitySize"
choices={communitySize}
handleFunction={(selectedValues: any) => {
field.onChange(selectedValues.name);
}}
description={
<>
Include your email list, social media following (e.g.,
Instagram, Discord, Twitter).
</>
}
placeholder="Select"
selectLabel="Select"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex space-x-5">
<Switch
onCheckedChange={() => setApplyProject(!applyProject)}
></Switch>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-[12px] text-neutral-500 self-center cursor-pointer">
Would you like to apply for your first fundraising project
as well?
</span>
</TooltipTrigger>
<TooltipContent>
<p className="text-[11px]">
Toggling this option allows you to begin your first
project, <br /> which is crucial for unlocking the tools
necessary to raise funds.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<center>
<Button
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5"
type="submit"
>
Submit application
</Button>
</center>
</div>
</div>
</form>
</Form>
);
};
export default BusinessForm;

View File

@ -0,0 +1,594 @@
import { useEffect, useState } from "react";
import { SubmitHandler, useForm, ControllerRenderProps } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { MultipleOptionSelector } from "@/components/multipleSelector";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { projectFormSchema } from "@/types/schemas/application.schema";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Label } from "@/components/ui/label";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { Textarea } from "./ui/textarea";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { ChevronsUpDown, Check, X } from "lucide-react";
type projectSchema = z.infer<typeof projectFormSchema>;
type FieldType = ControllerRenderProps<any, "projectPhotos">;
interface ProjectFormProps {
onSubmit: SubmitHandler<projectSchema>;
}
const ProjectForm = ({
onSubmit,
}: ProjectFormProps & { onSubmit: SubmitHandler<projectSchema> }) => {
const form = useForm<projectSchema>({
resolver: zodResolver(projectFormSchema),
defaultValues: {},
});
let supabase = createSupabaseClient();
const [projectType, setProjectType] = useState<
{ id: number; name: string }[]
>([]);
const [projectPitch, setProjectPitch] = useState("text");
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [projectPitchFile, setProjectPitchFile] = useState("");
const [tag, setTag] = useState<{ id: number; value: string }[]>([]);
const [open, setOpen] = useState(false);
const [selectedTag, setSelectedTag] = useState<
{ id: number; value: string }[]
>([]);
const handleFileChange = (
event: React.ChangeEvent<HTMLInputElement>,
field: FieldType
) => {
if (event.target.files) {
const filesArray = Array.from(event.target.files);
console.log("first file", filesArray);
setSelectedImages((prevImages) => {
const updatedImages = [...prevImages, ...filesArray];
console.log("Updated Images Array:", updatedImages);
field.onChange(updatedImages);
return updatedImages;
});
}
};
const handleRemoveImage = (index: number, field: FieldType) => {
setSelectedImages((prevImages) => {
const updatedImages = prevImages.filter((_, i) => i !== index);
console.log("After removal - Updated Images:", updatedImages);
field.onChange(updatedImages);
return updatedImages;
});
};
const fetchProjectType = async () => {
let { data: ProjectType, error } = await supabase
.from("project_type")
.select("id, value");
if (error) {
console.error(error);
} else {
if (ProjectType) {
setProjectType(
ProjectType.map((item) => ({
id: item.id,
name: item.value,
}))
);
}
}
};
const fetchTag = async () => {
let { data: tag, error } = await supabase.from("tag").select("id, value");
if (error) {
console.error(error);
} else {
if (tag) {
setTag(
tag.map((item) => ({
id: item.id,
value: item.value,
}))
);
}
}
};
useEffect(() => {
fetchProjectType();
fetchTag();
}, []);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)}
className="space-y-8"
>
<div className="ml-96 space-y-10">
{/* project name */}
<FormField
control={form.control}
name="projectName"
render={({ field }: { field: any }) => (
<FormItem>
<div className="space-y-5">
<FormLabel className="font-bold text-lg">
Project name
</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="text"
id="projectName"
className="w-96"
{...field}
/>
</div>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* project type */}
<FormField
control={form.control}
name="projectType"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<MultipleOptionSelector
header={<>Project type</>}
fieldName="projectType"
choices={projectType}
handleFunction={(selectedValues: any) => {
field.onChange(selectedValues.id);
}}
description={
<>Please specify the primary purpose of the funds</>
}
placeholder="Select a Project type"
selectLabel="Project type"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* short description */}
<FormField
control={form.control}
name="shortDescription"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Short description
</FormLabel>
<div className="flex space-x-5">
<Textarea
id="shortDescription"
className="w-96"
{...field}
/>
<span className="text-[12px] text-neutral-500 self-center">
Could you provide a brief description of your project{" "}
<br /> in one or two sentences?
</span>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Pitch Deck */}
<FormField
control={form.control}
name="projectPitchDeck"
render={({ field }) => (
<FormItem>
<div className="space-y-5 mt-10">
<Label htmlFor="pitchDeck" className="font-bold text-lg">
Pitch deck
</Label>
<FormControl>
<div>
<div className="flex space-x-2 w-96">
<Button
type="button"
variant={
projectPitch === "text" ? "default" : "outline"
}
onClick={() => setProjectPitch("text")}
className="w-32 h-12 text-base"
>
Paste URL
</Button>
<Button
type="button"
variant={
projectPitch === "file" ? "default" : "outline"
}
onClick={() => setProjectPitch("file")}
className="w-32 h-12 text-base"
>
Upload a file
</Button>
</div>
<div className="flex space-x-5">
<Input
type={projectPitch === "file" ? "file" : "text"}
placeholder={
projectPitch === "file"
? "Upload your Markdown file"
: "https:// "
}
accept={projectPitch === "file" ? ".md" : undefined}
onChange={(e) => {
const value = e.target;
if (projectPitch === "file") {
const file = value.files?.[0];
field.onChange(file || "");
} else {
field.onChange(value.value);
}
}}
className="w-96 mt-5"
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload a file or paste a link to your pitch,
which should <br />
cover key aspects of your project: what it will do,
what investors <br /> can expect to gain, and any
highlights that make it stand out.
</span>
</div>
{projectPitchFile && (
<div className="flex justify-between items-center border p-2 rounded w-96 text-sm text-foreground">
<span>1. {projectPitchFile}</span>
<Button
className="ml-4"
onClick={() => {
setProjectPitchFile("");
}}
>
Remove
</Button>
</div>
)}
</div>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
{/* project logo */}
<FormField
control={form.control}
name="projectLogo"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project logo
</FormLabel>
<Input
type="file"
id="projectLogo"
className="w-96"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || "");
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* project photos */}
<FormField
control={form.control}
name="projectPhotos"
render={({ field }: { field: any }) => (
<FormItem>
<FormControl>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg mt-10">
Project photos
</FormLabel>
<div className="flex space-x-5">
<Input
type="file"
id="projectPhotos"
multiple
accept="image/*"
className="w-96"
onChange={(event) => {
handleFileChange(event, field);
}}
/>
<span className="text-[12px] text-neutral-500 self-center">
Please upload the logo picture that best represents your
project.
</span>
</div>
<div className="mt-5 space-y-2 w-96">
{selectedImages.map((image, index) => (
<div
key={index}
className="flex justify-between items-center border p-2 rounded"
>
<span>{image.name}</span>
<Button
variant="outline"
onClick={() => handleRemoveImage(index, field)}
className="ml-4"
type="reset"
>
Remove
</Button>
</div>
))}
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Minimum investment */}
<FormField
control={form.control}
name="minInvest"
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Minimum investment
</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="number"
id="minInvest"
placeholder="$ 500"
className="w-96"
{...field}
onChange={(e) => {
const value = e.target.value;
field.onChange(value ? parseFloat(value) : null);
}}
value={field.value}
/>
<span className="text-[12px] text-neutral-500 self-center">
This helps set clear expectations for investors
</span>
</div>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Target investment */}
<FormField
control={form.control}
name="targetInvest"
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">
Target investment
</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="number"
id="targetInvest"
className="w-96"
placeholder="$ 1,000,000"
{...field}
onChange={(e) => {
const value = e.target.value;
field.onChange(value ? parseFloat(value) : null);
}}
value={field.value}
/>
<span className="text-[12px] text-neutral-500 self-center">
We encourage you to set a specific target investment{" "}
<br /> amount that reflects your funding goals.
</span>
</div>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Deadline */}
<FormField
control={form.control}
name="deadline"
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">Deadline</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Input
type="datetime-local"
id="deadline"
className="w-96"
{...field}
/>
<span className="text-[12px] text-neutral-500 self-center">
What is the deadline for your fundraising project?
Setting <br /> a clear timeline can help motivate
potential investors.
</span>
</div>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Tags */}
<FormField
control={form.control}
name="tag"
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<FormLabel className="font-bold text-lg">Tags</FormLabel>
<FormControl>
<div className="flex space-x-5">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-96 justify-between overflow-hidden text-ellipsis whitespace-nowrap"
>
{selectedTag.length > 0
? selectedTag.map((t) => t.value).join(", ")
: "Select tags..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-96 p-0">
<Command>
<CommandInput placeholder="Search tags..." />
<CommandList>
<CommandEmpty>No tags found.</CommandEmpty>
<CommandGroup>
{tag.map((tag) => (
<CommandItem
key={tag.id}
value={tag.value}
onSelect={() => {
setSelectedTag((prev) => {
const exists = prev.find(
(t) => t.id === tag.id
);
const updatedTags = exists
? prev.filter((t) => t.id !== tag.id)
: [...prev, tag];
field.onChange(
updatedTags.map((t) => t.id)
);
return updatedTags;
});
setOpen(false);
}}
>
<Check
className={cn(
"h-4",
selectedTag.some((t) => t.id === tag.id)
? "opacity-100"
: "opacity-0"
)}
/>
{tag.value}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<span className="text-[12px] text-neutral-500 self-center">
Add 1 to 5 tags that describe your project. Tags help{" "}
<br />
investors understand your focus.
</span>
</div>
</FormControl>
</div>
<FormMessage />
{/* display selected tags */}
<div className="flex flex-wrap space-x-3">
{selectedTag.map((tag) => (
<div
key={tag.id}
className="flex items-center space-x-1 p-1 rounded mt-2 outline outline-offset-2 outline-1"
>
<span>{tag.value}</span>
<button
onClick={() => {
setSelectedTag((prev) => {
const updatedTags = prev.filter(
(t) => t.id !== tag.id
);
field.onChange(updatedTags.map((t) => t.id));
return updatedTags;
});
}}
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</FormItem>
)}
/>
</div>
<center>
<Button
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5 "
type="submit"
>
Submit application
</Button>
</center>
</form>
</Form>
);
};
export default ProjectForm;

View File

@ -14,7 +14,7 @@ interface SelectorInterface {
export function DualOptionSelector(props: SelectorInterface) {
return (
<div className="space-y-5">
<div className="space-y-5 mt-10">
<Label htmlFor={props.name} className="font-bold text-lg">
{props.label}
</Label>
@ -23,7 +23,7 @@ export function DualOptionSelector(props: SelectorInterface) {
<Button
type="button"
variant={props.value === props.choice1 ? "default" : "outline"}
onClick={() => props.handleFunction(props.name, props.choice1)}
onClick={() => props.handleFunction(props.choice1)}
className="w-20 h-12 text-base"
>
{props.choice1}
@ -31,7 +31,7 @@ export function DualOptionSelector(props: SelectorInterface) {
<Button
type="button"
variant={props.value === props.choice2 ? "default" : "outline"}
onClick={() => props.handleFunction(props.name, props.choice2)}
onClick={() => props.handleFunction(props.choice2)}
className="w-20 h-12 text-base"
>
{props.choice2}

View File

@ -0,0 +1,27 @@
import Lottie from "react-lottie";
import * as loadingData from "./loading.json";
const loadingOption = {
loop: true,
autoplay: true,
animationData: loadingData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
interface LoaderProps {
isSuccess: boolean;
}
export function Loader(props: LoaderProps) {
return (
<>
{!props.isSuccess && (
<div className="fixed inset-0 flex items-center justify-center bg-white bg-opacity-10 backdrop-blur-sm z-50">
<Lottie options={loadingOption} height={200} width={200} />
</div>
)}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -8,19 +8,20 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ReactElement } from "react";
import { ReactElement, useState } from "react";
interface MultipleOptionSelector {
interface MultipleOptionSelectorProps {
header: ReactElement;
fieldName: string;
choices: string[];
handleFunction: Function;
choices: { id: number; name: string }[];
handleFunction: Function | null;
description: ReactElement;
placeholder: string;
selectLabel: string;
}
export function MultipleOptionSelector(props: MultipleOptionSelector) {
export function MultipleOptionSelector(props: MultipleOptionSelectorProps) {
const [value, setValue] = useState("");
return (
<div className="mt-10 space-y-5">
<Label htmlFor={props.fieldName} className="font-bold text-lg mt-10">
@ -28,9 +29,15 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
</Label>
<div className="flex space-x-5">
<Select
onValueChange={(value) => {
props.handleFunction(props.fieldName, value);
// console.log(value, props.fieldName);
value={value}
onValueChange={(id) => {
setValue(id);
const selectedChoice = props.choices.find(
(choice) => choice.id.toString() === id
);
if (selectedChoice && props.handleFunction) {
props.handleFunction(selectedChoice);
}
}}
>
<SelectTrigger className="w-96">
@ -40,8 +47,8 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
<SelectGroup>
<SelectLabel>{props.selectLabel}</SelectLabel>
{props.choices.map((i) => (
<SelectItem key={i} value={i}>
{i}
<SelectItem key={i.id} value={i.id.toString()}>
{i.name}
</SelectItem>
))}
</SelectGroup>

View File

@ -51,8 +51,8 @@ export function NavigationBar() {
const projectComponents = [
{
title: "Projects",
href: "/landing",
description: "Raise on B2DVentures",
href: "/project/apply",
description: "Start your new project on B2DVentures",
},
];

View File

@ -32,7 +32,7 @@ const UnAuthenticatedComponents = () => {
);
};
const AuthenticatedComponents = () => {
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
let notifications = 100;
const displayValue = notifications >= 100 ? "..." : notifications;
return (
@ -58,7 +58,7 @@ const AuthenticatedComponents = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Link href="/profile">Profile</Link>
<Link href={`/profile/${uid}`}>Profile</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
@ -88,7 +88,7 @@ export function ProfileBar() {
<>
{sessionLoaded ? (
user ? (
<AuthenticatedComponents />
<AuthenticatedComponents uid={user.id} />
) : (
<UnAuthenticatedComponents />
)

View File

@ -25,7 +25,7 @@ export function ProjectCard(props: ProjectCardProps) {
return (
<div
className={cn(
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px]",
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
props.className
)}>
<div className="flex flex-col h-full">
@ -58,7 +58,7 @@ export function ProjectCard(props: ProjectCardProps) {
{/* Info 1 */}
<div>
<div className="transition-transform duration-500 transform opacity-100 group-hover:opacity-0 p-4">
<div className="transition-transform duration-500 transform opacity-100 group-hover:opacity-0 p-4 ">
<div className="flex items-center text-muted-foreground">
<span className="flex items-center gap-1">
<CalendarDaysIcon width={20} />
@ -79,7 +79,7 @@ export function ProjectCard(props: ProjectCardProps) {
</div>
{/* Info 2 */}
<div className="hidden group-hover:flex group-hover:absolute group-hover:bottom-4 p-4">
<div className="hidden group-hover:flex group-hover:absolute group-hover:bottom-4 p-4 ">
{/* Info 2 (Visible on hover) */}
<div className="transition-transform duration-500 transform translate-y-6 opacity-0 group-hover:translate-y-0 group-hover:opacity-100">
<hr className="-ml-4 mb-2" />

View File

@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {

View File

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

178
src/components/ui/form.tsx Normal file
View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -2,7 +2,7 @@ import { SupabaseClient } from "@supabase/supabase-js";
async function getTopProjects(
client: SupabaseClient,
numberOfRecords: number = 4,
numberOfRecords: number = 4
) {
try {
const { data, error } = await client
@ -21,7 +21,7 @@ async function getTopProjects(
target_investment,
investment_deadline
),
item_tag (
project_tag (
tag (
id,
value
@ -30,7 +30,7 @@ async function getTopProjects(
business (
location
)
`,
`
)
.order("published_time", { ascending: false })
.limit(numberOfRecords);
@ -48,8 +48,10 @@ async function getTopProjects(
}
function getProjectDataQuery(client: SupabaseClient, projectId: number) {
return client.from("project").select(
`
return client
.from("project")
.select(
`
project_name,
project_short_description,
project_description,
@ -65,13 +67,17 @@ function getProjectDataQuery(client: SupabaseClient, projectId: number) {
tag_name:value
)
)
`,
).eq("id", projectId).single();
`
)
.eq("id", projectId)
.single();
}
async function getProjectData(client: SupabaseClient, projectId: number) {
const query = client.from("project").select(
`
const query = client
.from("project")
.select(
`
project_name,
project_short_description,
project_description,
@ -87,8 +93,10 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
tag_name:value
)
)
`,
).eq("id", projectId).single();
`
)
.eq("id", projectId)
.single();
const { data, error } = await query;
return { data, error };
@ -118,19 +126,21 @@ function searchProjectsQuery(
sortByTimeFilter,
page = 1,
pageSize = 4,
}: FilterProjectQueryParams,
}: FilterProjectQueryParams
) {
const start = (page - 1) * pageSize;
const end = start + pageSize - 1;
let query = client.from("project").select(
`
let query = client
.from("project")
.select(
`
project_id:id,
project_name,
published_time,
project_short_description,
card_image_url,
...project_status!project_project_status_id_fkey!inner (
...project_status!inner (
project_status:value
),
...project_investment_detail!inner (
@ -150,8 +160,10 @@ function searchProjectsQuery(
),
business_location:location
)
`,
).order("published_time", { ascending: false }).range(start, end);
`
)
.order("published_time", { ascending: false })
.range(start, end);
if (sortByTimeFilter === "all") {
sortByTimeFilter = undefined;

View File

@ -0,0 +1,163 @@
import { z } from "zod";
const MAX_FILE_SIZE = 500000;
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
const imageSchema = 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.",
});
const projectFormSchema = z.object({
projectName: z.string().min(5, {
message: "Project name must be at least 5 characters.",
}),
projectType: z.number({
required_error: "Please select one of the option",
}),
shortDescription: z
.string({
required_error: "Please provide a brief description for your project",
})
.min(10, {
message: "Short description must be at least 10 characters.",
}),
projectPitchDeck: z.union([
z
.string()
.url("Pitch deck must be a valid URL.")
.refine((url) => url.endsWith(".md"), {
message: "Pitch deck URL must link to a markdown file (.md).",
}),
z
.custom<File>((val) => val instanceof File, {
message: "Input must be a file.",
})
.refine((file) => file.size < MAX_FILE_SIZE, {
message: "File can't be bigger than 5MB.",
})
.refine((file) => file.name.endsWith(".md"), {
message: "File must be a markdown file (.md).",
}),
]),
projectLogo: imageSchema,
projectPhotos: z.custom(
(value) => {
if (value instanceof FileList || Array.isArray(value)) {
return (
value.length > 0 &&
Array.from(value).every((item) => item instanceof File)
);
}
return false;
},
{
message:
"Must be a FileList or an array of File objects with at least one file.",
}
),
minInvest: z
.number({
required_error: "Minimum investment must be a number.",
invalid_type_error: "Minimum investment must be a valid number.",
})
.positive()
.max(9999999999, "Minimum investment must be a realistic amount."),
targetInvest: z
.number({
required_error: "Target investment must be a number.",
invalid_type_error: "Target investment must be a valid number.",
})
.positive()
.max(9999999999, "Target investment must be a realistic amount."),
deadline: z
.string()
.min(1, "Deadline is required.")
.refine((value) => !isNaN(Date.parse(value)), {
message: "Invalid date-time format.",
})
.transform((value) => new Date(value))
.refine((date) => date > new Date(), {
message: "Deadline must be in the future.",
}),
tag: z
.array(z.number())
.min(1, "Please provide at least one tag.")
.max(5, "You can provide up to 5 tags."),
});
const businessFormSchema = z.object({
companyName: z.string().min(5, {
message: "Company name must be at least 5 characters.",
}),
industry: z.number({
required_error: "Please select one of the option",
}),
isInUS: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
isForSale: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
isGenerating: z
.string({
required_error: "Please select either 'Yes' or 'No'.",
})
.transform((val) => val.toLowerCase())
.refine((val) => val === "yes" || val === "no", {
message: "Please select either 'Yes' or 'No'.",
}),
totalRaised: z
.number({
required_error: "Total raised must be a number.",
invalid_type_error: "Total raised must be a valid number.",
})
.positive()
.max(9999999999, "Total raised must be a realistic amount."),
communitySize: z.string({
required_error: "Please select one of the option",
}),
businessPitchDeck: z.union([
z
.string()
.url("Pitch deck must be a valid URL.")
.refine((url) => url.endsWith(".md"), {
message: "Pitch deck URL must link to a markdown file (.md).",
}),
z
.custom<File>((val) => val instanceof File, {
message: "Input must be a file.",
})
.refine((file) => file.size < MAX_FILE_SIZE, {
message: "File can't be bigger than 5MB.",
})
.refine((file) => file.name.toLowerCase().endsWith(".md"), {
message: "File must be a markdown file (.md).",
}),
]),
country: z.string({
required_error: "Please select one of the option",
}),
});
export { businessFormSchema, projectFormSchema };