mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-24 00:14:04 +01:00
Merge pull request #66 from Sosokker/front-end
Business and Application form UI + logic
This commit is contained in:
commit
548e9babe0
728
package-lock.json
generated
728
package-lock.json
generated
@ -9,12 +9,14 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.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-progress": "^1.1.0",
|
||||||
"@radix-ui/react-radio-group": "^1.2.1",
|
"@radix-ui/react-radio-group": "^1.2.1",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
@ -33,6 +35,7 @@
|
|||||||
"b2d-ventures": "file:",
|
"b2d-ventures": "file:",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
@ -44,9 +47,11 @@
|
|||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-lottie": "^1.2.4",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"stripe": "^17.1.0",
|
"stripe": "^17.1.0",
|
||||||
|
"sweetalert2": "^11.14.3",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
@ -55,9 +60,13 @@
|
|||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/next": "^8.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^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": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
@ -932,6 +941,33 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
"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": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.16.10",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
@ -2243,6 +2341,17 @@
|
|||||||
"undici-types": "~6.19.2"
|
"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": {
|
"node_modules/@types/phoenix": {
|
||||||
"version": "1.6.5",
|
"version": "1.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
|
||||||
@ -2271,11 +2380,151 @@
|
|||||||
"@types/react": "*"
|
"@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": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||||
@ -2700,6 +2949,13 @@
|
|||||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@ -2737,6 +2993,20 @@
|
|||||||
"resolved": "",
|
"resolved": "",
|
||||||
"link": true
|
"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": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"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": "^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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"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": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@ -3055,6 +3698,13 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/core-js-pure": {
|
||||||
"version": "3.38.1",
|
"version": "3.38.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.38.1.tgz",
|
"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"
|
"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": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
@ -4322,6 +4982,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/formdata-polyfill": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
@ -5492,6 +6167,11 @@
|
|||||||
"loose-envify": "cli.js"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
@ -6104,6 +6784,29 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"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": {
|
"node_modules/react-markdown": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
|
||||||
@ -7867,6 +8585,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
|
||||||
|
|||||||
@ -10,12 +10,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.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-progress": "^1.1.0",
|
||||||
"@radix-ui/react-radio-group": "^1.2.1",
|
"@radix-ui/react-radio-group": "^1.2.1",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
@ -34,6 +36,7 @@
|
|||||||
"b2d-ventures": "file:",
|
"b2d-ventures": "file:",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
@ -45,9 +48,11 @@
|
|||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-lottie": "^1.2.4",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"stripe": "^17.1.0",
|
"stripe": "^17.1.0",
|
||||||
|
"sweetalert2": "^11.14.3",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
@ -56,9 +61,13 @@
|
|||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/next": "^8.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^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": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
|||||||
1420
pnpm-lock.yaml
1420
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -125,7 +125,7 @@ export default async function ProjectDealPage({ params }: { params: { id: number
|
|||||||
<CardDescription></CardDescription>
|
<CardDescription></CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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>
|
<ReactMarkdown>{projectData?.project_description || "No pitch available."}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
// components/ProfilePage.tsx
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
@ -10,22 +8,11 @@ import ReactMarkdown from "react-markdown";
|
|||||||
|
|
||||||
interface Profile extends Tables<"Profiles"> {}
|
interface Profile extends Tables<"Profiles"> {}
|
||||||
|
|
||||||
export default async function ProfilePage() {
|
export default async function ProfilePage({ params }: { params: { uid: string } }) {
|
||||||
const supabase = createSupabaseClient();
|
const supabase = createSupabaseClient();
|
||||||
|
const uid = params.uid;
|
||||||
|
|
||||||
const {
|
const { data: profileData, error } = await getUserProfile(supabase, uid);
|
||||||
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);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
44
src/app/api/dealApi.ts
Normal file
44
src/app/api/dealApi.ts
Normal 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
100
src/app/api/generalApi.ts
Normal 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
13
src/app/api/userApi.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -1,834 +1,185 @@
|
|||||||
"use client";
|
"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 { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
import { useEffect, useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { SubmitHandler } from "react-hook-form";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import BusinessForm from "@/components/BusinessForm";
|
||||||
import { DualOptionSelector } from "@/components/dualSelector";
|
import { businessFormSchema } from "@/types/schemas/application.schema";
|
||||||
import { MultipleOptionSelector } from "@/components/multipleSelector";
|
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() {
|
type businessSchema = z.infer<typeof businessFormSchema>;
|
||||||
const [industry, setIndustry] = useState<string[]>([]);
|
const BUCKET_PITCH_NAME = "business-application";
|
||||||
const [isInUS, setIsInUS] = useState("");
|
let supabase = createSupabaseClient();
|
||||||
const [isForSale, setIsForSale] = useState("");
|
|
||||||
const [isGenerating, setIsGenerating] = useState("");
|
export default function ApplyBusiness() {
|
||||||
const [businessPitch, setBusinessPitch] = useState("text");
|
|
||||||
const [projectType, setProjectType] = useState<string[]>([]);
|
|
||||||
const [projectPitch, setProjectPitch] = useState("text");
|
|
||||||
const [applyProject, setApplyProject] = useState(false);
|
const [applyProject, setApplyProject] = useState(false);
|
||||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
const alertShownRef = useRef(false);
|
||||||
const [businessPitchFile, setBusinessPitchFile] = useState("");
|
const [success, setSucess] = useState(false);
|
||||||
const [projectPitchFile, setProjectPitchFile] = useState("");
|
|
||||||
const MAX_FILE_SIZE = 5000000;
|
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
|
||||||
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
|
const transformedData = await transformChoice(data);
|
||||||
const createPitchDeckSchema = (inputType: string) => {
|
await sendApplication(transformedData);
|
||||||
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 imageSchema = z
|
const sendApplication = async (recvData: any) => {
|
||||||
.custom<File>((val) => val && typeof val === "object" && "size" in val && "type" in val, {
|
setSucess(false);
|
||||||
message: "Input must be a file.",
|
const {
|
||||||
})
|
data: { user },
|
||||||
.refine((file) => file.size < MAX_FILE_SIZE, {
|
} = await supabase.auth.getUser();
|
||||||
message: "File can't be bigger than 5MB.",
|
const pitchType = typeof recvData["businessPitchDeck"];
|
||||||
})
|
if (pitchType === "object") {
|
||||||
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
if (user?.id) {
|
||||||
message: "File format must be either jpg, jpeg, or png.",
|
const uploadSuccess = await uploadFile(
|
||||||
});
|
recvData["businessPitchDeck"],
|
||||||
|
BUCKET_PITCH_NAME,
|
||||||
|
// file structure: userId/fileName
|
||||||
|
`${user?.id}/pitch-file/pitch.md`
|
||||||
|
);
|
||||||
|
|
||||||
const projectFormSchema = z.object({
|
if (!uploadSuccess) {
|
||||||
projectName: z.string().min(5, {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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+"];
|
console.log("file upload successful");
|
||||||
|
} else {
|
||||||
useEffect(() => {
|
console.error("user ID is undefined.");
|
||||||
register("industry");
|
return;
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveImage = (index: number) => {
|
const { data, error } = await supabase
|
||||||
setSelectedImages((prevImages) => {
|
.from("business_application")
|
||||||
const updatedImages = prevImages.filter((_, i) => i !== index);
|
.insert([
|
||||||
console.log("After removal - Updated Images:", updatedImages);
|
{
|
||||||
// ensure we're setting an array of File objects
|
user_id: user?.id,
|
||||||
setValueProject("projectPhotos", updatedImages);
|
business_name: recvData["companyName"],
|
||||||
return updatedImages;
|
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[] => {
|
const hasUserApplied = async (userID: string) => {
|
||||||
if (Array.isArray(value)) return value;
|
let { data: business, error } = await supabase
|
||||||
if (value instanceof File) return [value];
|
.from("business")
|
||||||
return [];
|
.select("*")
|
||||||
|
.eq("user_id", userID);
|
||||||
|
console.table(business);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
if (business) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformChoice = (data: any) => {
|
const transformChoice = (data: any) => {
|
||||||
// convert any yes and no to true or false
|
// convert any yes and no to true or false
|
||||||
const transformedData = Object.entries(data).reduce((acc: Record<any, any>, [key, value]) => {
|
const transformedData = Object.entries(data).reduce(
|
||||||
if (typeof value === "string") {
|
(acc: Record<any, any>, [key, value]) => {
|
||||||
const lowerValue = value.toLowerCase();
|
if (typeof value === "string") {
|
||||||
if (lowerValue === "yes") {
|
const lowerValue = value.toLowerCase();
|
||||||
acc[key] = true;
|
if (lowerValue === "yes") {
|
||||||
} else if (lowerValue === "no") {
|
acc[key] = true;
|
||||||
acc[key] = false;
|
} else if (lowerValue === "no") {
|
||||||
|
acc[key] = false;
|
||||||
|
} else {
|
||||||
|
acc[key] = value; // keep other string values unchanged
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
acc[key] = value; // keep other string values unchanged
|
acc[key] = value; // keep other types unchanged
|
||||||
}
|
}
|
||||||
} else {
|
return acc;
|
||||||
acc[key] = value; // keep other types unchanged
|
},
|
||||||
}
|
{}
|
||||||
return acc;
|
);
|
||||||
}, {});
|
|
||||||
return transformedData;
|
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(() => {
|
useEffect(() => {
|
||||||
fetchIndustry();
|
const fetchUserData = async () => {
|
||||||
fetchProjectType();
|
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 (
|
return (
|
||||||
<div>
|
<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">
|
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-100 dark:bg-gray-800 p-5">
|
||||||
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
|
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
|
||||||
Apply to raise on B2DVentures
|
Apply to raise on B2DVentures
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-5 justify-self-center">
|
<div className="mt-5 justify-self-center">
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
All information submitted in this application is for internal use 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>
|
||||||
<p className="text-sm md:text-base text-neutral-500">
|
<p className="text-sm md:text-base text-neutral-500">
|
||||||
confidentiality. Companies may apply to raise with B2DVentures more than once.
|
confidentiality. Companies may apply to raise with B2DVentures more
|
||||||
|
than once.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* form */}
|
{/* form */}
|
||||||
<form action="" onSubmit={handleSubmit(handleSubmitForms)}>
|
{/* <form action="" onSubmit={handleSubmit(handleSubmitForms)}> */}
|
||||||
<div className="grid grid-flow-row auto-rows-max w-3/4 ml-1/2 lg:ml-[10%]">
|
<BusinessForm
|
||||||
<h1 className="text-3xl font-bold mt-10 ml-96">About your company</h1>
|
onSubmit={onSubmit}
|
||||||
<p className="ml-96 mt-5 text-neutral-500">
|
applyProject={applyProject}
|
||||||
<span className="text-red-500 font-bold">**</span>All requested information in this section is required.
|
setApplyProject={setApplyProject}
|
||||||
</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). We’d 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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/app/dashboard/hook.ts
Normal file
17
src/app/dashboard/hook.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -12,10 +12,22 @@ import { Overview } from "@/components/ui/overview";
|
|||||||
import { RecentFunds } from "@/components/recent-funds";
|
import { RecentFunds } from "@/components/recent-funds";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useDealList } from "./hook";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [graphType, setGraphType] = useState("line");
|
const [graphType, setGraphType] = useState("line");
|
||||||
|
const dealList = useDealList();
|
||||||
|
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{dealList?.map((deal, index) => (
|
||||||
|
<div key={index} className="deal-item">
|
||||||
|
<p>Deal Amount: {deal.deal_amount}</p>
|
||||||
|
<p>Created Time: {new Date(deal.created_time).toUTCString()}</p>
|
||||||
|
<p>Investor ID: {deal.investor_id}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Image
|
<Image
|
||||||
src="/examples/dashboard-light.png"
|
src="/examples/dashboard-light.png"
|
||||||
@ -35,7 +47,7 @@ export default function Dashboard() {
|
|||||||
<div className="hidden flex-col md:flex">
|
<div className="hidden flex-col md:flex">
|
||||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||||
<div className="flex items-center justify-between space-y-2">
|
<div className="flex items-center justify-between space-y-2">
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="overview" className="space-y-4">
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
@ -63,10 +75,10 @@ export default function Dashboard() {
|
|||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">$45,231.89</div>
|
<div className="text-2xl font-bold">${totalDealAmount}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
{/* <p className="text-xs text-muted-foreground">
|
||||||
+20.1% from last month
|
+20.1% from last month
|
||||||
</p>
|
</p> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
@ -90,9 +102,9 @@ export default function Dashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">+2350</div>
|
<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
|
+180.1% from last month
|
||||||
</p>
|
</p> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
@ -117,9 +129,9 @@ export default function Dashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">+12,234</div>
|
<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
|
+19% from last month
|
||||||
</p>
|
</p> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{/* <Card>
|
{/* <Card>
|
||||||
@ -181,11 +193,12 @@ export default function Dashboard() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Funds</CardTitle>
|
<CardTitle>Recent Funds</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You made 265 sales this month.
|
You made {dealList?.length || 0} sales this month.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<RecentFunds />
|
<RecentFunds>
|
||||||
|
</RecentFunds>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,39 +1,63 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { 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 { Separator } from "@/components/ui/separator";
|
||||||
import { ProjectCard } from "@/components/projectCard";
|
import { ProjectCard } from "@/components/projectCard";
|
||||||
import { getTopProjects } from "@/lib/data/projectQuery";
|
import { getTopProjects } from "@/lib/data/projectQuery";
|
||||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
const TopProjects = async () => {
|
interface Project {
|
||||||
const supabase = createSupabaseClient();
|
id: number;
|
||||||
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
|
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) {
|
interface TopProjectsProps {
|
||||||
return <div>Error loading top projects: {topProjectsError}</div>;
|
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>No top projects available.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<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}>
|
<Link href={`/deals/${project.id}`} key={project.id}>
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
name={project.project_name}
|
name={project.project_name}
|
||||||
description={project.project_short_description}
|
description={project.project_short_description}
|
||||||
imageUri={project.card_image_url}
|
imageUri={project.card_image_url}
|
||||||
joinDate={new Date(project.published_time).toLocaleDateString()}
|
joinDate={new Date(project.published_time).toLocaleDateString()}
|
||||||
location={project.business.location}
|
location={project.business[0]?.location || ""}
|
||||||
tags={project.item_tag.map((item) => item.tag.value)}
|
tags={project.project_tag.flatMap(
|
||||||
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
|
(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}
|
totalInvestor={0}
|
||||||
totalRaised={project.project_investment_detail[0]?.total_investment || 0}
|
totalRaised={
|
||||||
|
project.project_investment_detail[0]?.total_investment || 0
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@ -44,12 +68,18 @@ const TopProjects = async () => {
|
|||||||
const ProjectsLoader = () => (
|
const ProjectsLoader = () => (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{[...Array(4)].map((_, index) => (
|
{[...Array(4)].map((_, index) => (
|
||||||
<div 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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
const supabase = createSupabaseClient();
|
||||||
|
const { data: topProjectsData, error: topProjectsError } =
|
||||||
|
await getTopProjects(supabase);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="relative mx-auto">
|
<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="flex flex-row bg-slate-100 dark:bg-gray-800">
|
||||||
<div className="container max-w-screen-xl flex flex-col">
|
<div className="container max-w-screen-xl flex flex-col">
|
||||||
<span className="mx-20 px-10 py-10">
|
<span className="mx-20 px-10 py-10">
|
||||||
<p className="text-4xl font-bold">Explore the world of ventures</p>
|
<p className="text-4xl font-bold">
|
||||||
|
Explore the world of ventures
|
||||||
|
</p>
|
||||||
<span className="text-lg">
|
<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>investors and innovators.</p>
|
||||||
<p>Together, we turn ideas into impact.</p>
|
<p>Together, we turn ideas into impact.</p>
|
||||||
</span>
|
</span>
|
||||||
@ -107,11 +142,23 @@ export default async function Home() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex gap-2">
|
<CardContent className="flex gap-2">
|
||||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||||
<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
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||||
<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
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -123,10 +170,12 @@ export default async function Home() {
|
|||||||
<div className="flex flex-col px-10">
|
<div className="flex flex-col px-10">
|
||||||
<span className="pb-5">
|
<span className="pb-5">
|
||||||
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
||||||
<p className="text-md md:text-lg">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>
|
</span>
|
||||||
<Suspense fallback={<ProjectsLoader />}>
|
<Suspense fallback={<ProjectsLoader />}>
|
||||||
<TopProjects />
|
<TopProjects projects={topProjectsData || []} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div className="self-center py-5 scale-75 md:scale-100">
|
<div className="self-center py-5 scale-75 md:scale-100">
|
||||||
<Button>
|
<Button>
|
||||||
|
|||||||
282
src/app/project/apply/page.tsx
Normal file
282
src/app/project/apply/page.tsx
Normal 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 it’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
501
src/components/BusinessForm.tsx
Normal file
501
src/components/BusinessForm.tsx
Normal 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;
|
||||||
594
src/components/ProjectForm.tsx
Normal file
594
src/components/ProjectForm.tsx
Normal 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;
|
||||||
@ -14,7 +14,7 @@ interface SelectorInterface {
|
|||||||
|
|
||||||
export function DualOptionSelector(props: SelectorInterface) {
|
export function DualOptionSelector(props: SelectorInterface) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5 mt-10">
|
||||||
<Label htmlFor={props.name} className="font-bold text-lg">
|
<Label htmlFor={props.name} className="font-bold text-lg">
|
||||||
{props.label}
|
{props.label}
|
||||||
</Label>
|
</Label>
|
||||||
@ -23,7 +23,7 @@ export function DualOptionSelector(props: SelectorInterface) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={props.value === props.choice1 ? "default" : "outline"}
|
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"
|
className="w-20 h-12 text-base"
|
||||||
>
|
>
|
||||||
{props.choice1}
|
{props.choice1}
|
||||||
@ -31,7 +31,7 @@ export function DualOptionSelector(props: SelectorInterface) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={props.value === props.choice2 ? "default" : "outline"}
|
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"
|
className="w-20 h-12 text-base"
|
||||||
>
|
>
|
||||||
{props.choice2}
|
{props.choice2}
|
||||||
|
|||||||
27
src/components/loading/loader.tsx
Normal file
27
src/components/loading/loader.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
2549
src/components/loading/loading.json
Normal file
2549
src/components/loading/loading.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,19 +8,20 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement, useState } from "react";
|
||||||
|
|
||||||
interface MultipleOptionSelector {
|
interface MultipleOptionSelectorProps {
|
||||||
header: ReactElement;
|
header: ReactElement;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
choices: string[];
|
choices: { id: number; name: string }[];
|
||||||
handleFunction: Function;
|
handleFunction: Function | null;
|
||||||
description: ReactElement;
|
description: ReactElement;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
selectLabel: string;
|
selectLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
export function MultipleOptionSelector(props: MultipleOptionSelectorProps) {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 space-y-5">
|
<div className="mt-10 space-y-5">
|
||||||
<Label htmlFor={props.fieldName} className="font-bold text-lg mt-10">
|
<Label htmlFor={props.fieldName} className="font-bold text-lg mt-10">
|
||||||
@ -28,9 +29,15 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
|||||||
</Label>
|
</Label>
|
||||||
<div className="flex space-x-5">
|
<div className="flex space-x-5">
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
value={value}
|
||||||
props.handleFunction(props.fieldName, value);
|
onValueChange={(id) => {
|
||||||
// console.log(value, props.fieldName);
|
setValue(id);
|
||||||
|
const selectedChoice = props.choices.find(
|
||||||
|
(choice) => choice.id.toString() === id
|
||||||
|
);
|
||||||
|
if (selectedChoice && props.handleFunction) {
|
||||||
|
props.handleFunction(selectedChoice);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-96">
|
<SelectTrigger className="w-96">
|
||||||
@ -40,8 +47,8 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
|||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{props.selectLabel}</SelectLabel>
|
<SelectLabel>{props.selectLabel}</SelectLabel>
|
||||||
{props.choices.map((i) => (
|
{props.choices.map((i) => (
|
||||||
<SelectItem key={i} value={i}>
|
<SelectItem key={i.id} value={i.id.toString()}>
|
||||||
{i}
|
{i.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@ -51,8 +51,8 @@ export function NavigationBar() {
|
|||||||
const projectComponents = [
|
const projectComponents = [
|
||||||
{
|
{
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
href: "/landing",
|
href: "/project/apply",
|
||||||
description: "Raise on B2DVentures",
|
description: "Start your new project on B2DVentures",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const UnAuthenticatedComponents = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthenticatedComponents = () => {
|
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
||||||
let notifications = 100;
|
let notifications = 100;
|
||||||
const displayValue = notifications >= 100 ? "..." : notifications;
|
const displayValue = notifications >= 100 ? "..." : notifications;
|
||||||
return (
|
return (
|
||||||
@ -58,7 +58,7 @@ const AuthenticatedComponents = () => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href="/profile">Profile</Link>
|
<Link href={`/profile/${uid}`}>Profile</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
@ -88,7 +88,7 @@ export function ProfileBar() {
|
|||||||
<>
|
<>
|
||||||
{sessionLoaded ? (
|
{sessionLoaded ? (
|
||||||
user ? (
|
user ? (
|
||||||
<AuthenticatedComponents />
|
<AuthenticatedComponents uid={user.id} />
|
||||||
) : (
|
) : (
|
||||||
<UnAuthenticatedComponents />
|
<UnAuthenticatedComponents />
|
||||||
)
|
)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px]",
|
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
|
||||||
props.className
|
props.className
|
||||||
)}>
|
)}>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@ -58,7 +58,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
|||||||
|
|
||||||
{/* Info 1 */}
|
{/* Info 1 */}
|
||||||
<div>
|
<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">
|
<div className="flex items-center text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<CalendarDaysIcon width={20} />
|
<CalendarDaysIcon width={20} />
|
||||||
@ -79,7 +79,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info 2 */}
|
{/* 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) */}
|
{/* 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">
|
<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" />
|
<hr className="-ml-4 mb-2" />
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
155
src/components/ui/command.tsx
Normal file
155
src/components/ui/command.tsx
Normal 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
178
src/components/ui/form.tsx
Normal 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,
|
||||||
|
}
|
||||||
31
src/components/ui/popover.tsx
Normal file
31
src/components/ui/popover.tsx
Normal 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 }
|
||||||
@ -2,7 +2,7 @@ import { SupabaseClient } from "@supabase/supabase-js";
|
|||||||
|
|
||||||
async function getTopProjects(
|
async function getTopProjects(
|
||||||
client: SupabaseClient,
|
client: SupabaseClient,
|
||||||
numberOfRecords: number = 4,
|
numberOfRecords: number = 4
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
@ -21,7 +21,7 @@ async function getTopProjects(
|
|||||||
target_investment,
|
target_investment,
|
||||||
investment_deadline
|
investment_deadline
|
||||||
),
|
),
|
||||||
item_tag (
|
project_tag (
|
||||||
tag (
|
tag (
|
||||||
id,
|
id,
|
||||||
value
|
value
|
||||||
@ -30,7 +30,7 @@ async function getTopProjects(
|
|||||||
business (
|
business (
|
||||||
location
|
location
|
||||||
)
|
)
|
||||||
`,
|
`
|
||||||
)
|
)
|
||||||
.order("published_time", { ascending: false })
|
.order("published_time", { ascending: false })
|
||||||
.limit(numberOfRecords);
|
.limit(numberOfRecords);
|
||||||
@ -48,8 +48,10 @@ async function getTopProjects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
||||||
return client.from("project").select(
|
return client
|
||||||
`
|
.from("project")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
project_name,
|
project_name,
|
||||||
project_short_description,
|
project_short_description,
|
||||||
project_description,
|
project_description,
|
||||||
@ -65,13 +67,17 @@ function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
|||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`,
|
`
|
||||||
).eq("id", projectId).single();
|
)
|
||||||
|
.eq("id", projectId)
|
||||||
|
.single();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProjectData(client: SupabaseClient, projectId: number) {
|
async function getProjectData(client: SupabaseClient, projectId: number) {
|
||||||
const query = client.from("project").select(
|
const query = client
|
||||||
`
|
.from("project")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
project_name,
|
project_name,
|
||||||
project_short_description,
|
project_short_description,
|
||||||
project_description,
|
project_description,
|
||||||
@ -87,8 +93,10 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
|||||||
tag_name:value
|
tag_name:value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`,
|
`
|
||||||
).eq("id", projectId).single();
|
)
|
||||||
|
.eq("id", projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
const { data, error } = await query;
|
const { data, error } = await query;
|
||||||
return { data, error };
|
return { data, error };
|
||||||
@ -118,19 +126,21 @@ function searchProjectsQuery(
|
|||||||
sortByTimeFilter,
|
sortByTimeFilter,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 4,
|
pageSize = 4,
|
||||||
}: FilterProjectQueryParams,
|
}: FilterProjectQueryParams
|
||||||
) {
|
) {
|
||||||
const start = (page - 1) * pageSize;
|
const start = (page - 1) * pageSize;
|
||||||
const end = start + pageSize - 1;
|
const end = start + pageSize - 1;
|
||||||
|
|
||||||
let query = client.from("project").select(
|
let query = client
|
||||||
`
|
.from("project")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
project_id:id,
|
project_id:id,
|
||||||
project_name,
|
project_name,
|
||||||
published_time,
|
published_time,
|
||||||
project_short_description,
|
project_short_description,
|
||||||
card_image_url,
|
card_image_url,
|
||||||
...project_status!project_project_status_id_fkey!inner (
|
...project_status!inner (
|
||||||
project_status:value
|
project_status:value
|
||||||
),
|
),
|
||||||
...project_investment_detail!inner (
|
...project_investment_detail!inner (
|
||||||
@ -150,8 +160,10 @@ function searchProjectsQuery(
|
|||||||
),
|
),
|
||||||
business_location:location
|
business_location:location
|
||||||
)
|
)
|
||||||
`,
|
`
|
||||||
).order("published_time", { ascending: false }).range(start, end);
|
)
|
||||||
|
.order("published_time", { ascending: false })
|
||||||
|
.range(start, end);
|
||||||
|
|
||||||
if (sortByTimeFilter === "all") {
|
if (sortByTimeFilter === "all") {
|
||||||
sortByTimeFilter = undefined;
|
sortByTimeFilter = undefined;
|
||||||
|
|||||||
163
src/types/schemas/application.schema.ts
Normal file
163
src/types/schemas/application.schema.ts
Normal 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 };
|
||||||
Loading…
Reference in New Issue
Block a user