mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 21:44:06 +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",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
@ -33,6 +35,7 @@
|
||||
"b2d-ventures": "file:",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
@ -44,9 +47,11 @@
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-lottie": "^1.2.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"recharts": "^2.12.7",
|
||||
"stripe": "^17.1.0",
|
||||
"sweetalert2": "^11.14.3",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
@ -55,9 +60,13 @@
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/next": "^8.0.7",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-fade-in": "^2.0.2",
|
||||
"@types/react-lottie": "^1.2.10",
|
||||
"@types/react-select-country-list": "^2.2.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"postcss": "^8",
|
||||
@ -932,6 +941,33 @@
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz",
|
||||
"integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dialog": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||
@ -1330,6 +1366,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
|
||||
"integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.0",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||
@ -2235,6 +2307,32 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
},
|
||||
"node_modules/@types/next": {
|
||||
"version": "8.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/next/-/next-8.0.7.tgz",
|
||||
"integrity": "sha512-I/Gcj1YfOFmpBBX5XgBP1t1wKcFS0TGk8ytW99ujjvCp8U31QuKqM3fvvGb7+Hf1CJt3BAAgzGT0aCigqO5opQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/next-server": "*",
|
||||
"@types/node": "*",
|
||||
"@types/node-fetch": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/next-server": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/next-server/-/next-server-8.1.2.tgz",
|
||||
"integrity": "sha512-Fm4QhAxwDlC9AHiGy23Lhv7DeTTt1O1s7tnAsyVOLPjePmYXPZVbOCrxd2oRHZnIIYWw41JelLbq4hN1B5idlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/next": "*",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
@ -2243,6 +2341,17 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
|
||||
@ -2271,11 +2380,151 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-fade-in": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-fade-in/-/react-fade-in-2.0.2.tgz",
|
||||
"integrity": "sha512-JdyLYFtyvqDP7mqnKaAyuYD+VMtzAHbUf3kumNQV5QALxjBGmb95HXD0uug1bGol053dtV5yO3NNpGHOMj413g==",
|
||||
"deprecated": "This is a stub types definition. react-fade-in provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"react-fade-in": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-fade-in/node_modules/react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-fade-in/node_modules/react-fade-in": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-fade-in/-/react-fade-in-2.0.1.tgz",
|
||||
"integrity": "sha512-oqS/WT4znaXEHmL+yo0IDUDY7uC9K4RP35j1SdRUEBspR09B2iIC0i8oJ28tPOr6Ez/L2aktF9p89j+DbsTVNw==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || 17"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-loadable": {
|
||||
"version": "5.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.11.tgz",
|
||||
"integrity": "sha512-/tq2IJ853MoIFRBmqVOxnGsRRjER5TmEKzsZtaAkiXAWoDeKgR/QNOT1vd9k0p9h/F616X21cpNh3hu4RutzRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/webpack": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-lottie": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-lottie/-/react-lottie-1.2.10.tgz",
|
||||
"integrity": "sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-select-country-list": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
|
||||
"integrity": "sha512-nffcYOwuun+5B0EWqubK+amHpPdK9Xj20xkLYNqYrzmESd8FnpLwHsS79ClLAWA9y+icVA8gWPkbwBp1gpjSwA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/source-list-map": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz",
|
||||
"integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tapable": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz",
|
||||
"integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uglify-js": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz",
|
||||
"integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uglify-js/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
||||
},
|
||||
"node_modules/@types/webpack": {
|
||||
"version": "4.41.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.39.tgz",
|
||||
"integrity": "sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/tapable": "^1",
|
||||
"@types/uglify-js": "*",
|
||||
"@types/webpack-sources": "*",
|
||||
"anymatch": "^3.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/source-list-map": "*",
|
||||
"source-map": "^0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webpack-sources/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webpack/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||
@ -2700,6 +2949,13 @@
|
||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@ -2737,6 +2993,20 @@
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
|
||||
"dependencies": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-runtime/node_modules/regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
@ -3008,6 +3278,366 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
|
||||
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "1.0.5",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
|
||||
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
|
||||
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
|
||||
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
|
||||
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-focus-guards": "1.0.1",
|
||||
"@radix-ui/react-focus-scope": "1.0.4",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
|
||||
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
|
||||
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
|
||||
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
|
||||
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
|
||||
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
|
||||
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
|
||||
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
|
||||
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
|
||||
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
|
||||
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/react-remove-scroll": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -3024,6 +3654,19 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@ -3055,6 +3698,13 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||
"hasInstallScript": true
|
||||
},
|
||||
"node_modules/core-js-pure": {
|
||||
"version": "3.38.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.38.1.tgz",
|
||||
@ -3388,6 +4038,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@ -4322,6 +4982,21 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@ -5492,6 +6167,11 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lottie-web": {
|
||||
"version": "5.12.2",
|
||||
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
|
||||
"integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
@ -6104,6 +6784,29 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -7006,6 +7709,21 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-lottie": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lottie/-/react-lottie-1.2.4.tgz",
|
||||
"integrity": "sha512-kBGxI+MIZGBf4wZhNCWwHkMcVP+kbpmrLWH/SkO0qCKc7D7eSPcxQbfpsmsCo8v2KCBYjuGSou+xTqK44D/jMg==",
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"lottie-web": "^5.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
|
||||
@ -7867,6 +8585,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sweetalert2": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.14.3.tgz",
|
||||
"integrity": "sha512-6NuBHWJCv2gtw4y8PUXLB41hty+V6U2mKZMAvydL1IRPcORR0yuyq3cjFD/+ByrCk3muEFggbZX/x6HwmbVfbA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/limonte"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
|
||||
|
||||
@ -10,12 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
@ -34,6 +36,7 @@
|
||||
"b2d-ventures": "file:",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
@ -45,9 +48,11 @@
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-lottie": "^1.2.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"recharts": "^2.12.7",
|
||||
"stripe": "^17.1.0",
|
||||
"sweetalert2": "^11.14.3",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
@ -56,9 +61,13 @@
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/next": "^8.0.7",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-fade-in": "^2.0.2",
|
||||
"@types/react-lottie": "^1.2.10",
|
||||
"@types/react-select-country-list": "^2.2.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"postcss": "^8",
|
||||
|
||||
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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<div className="prose prose-sm max-w-none ">
|
||||
<ReactMarkdown>{projectData?.project_description || "No pitch available."}</ReactMarkdown>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// components/ProfilePage.tsx
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||
@ -10,22 +8,11 @@ import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface Profile extends Tables<"Profiles"> {}
|
||||
|
||||
export default async function ProfilePage() {
|
||||
export default async function ProfilePage({ params }: { params: { uid: string } }) {
|
||||
const supabase = createSupabaseClient();
|
||||
const uid = params.uid;
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<p className="text-red-500">No user found!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { data: profileData, error } = await getUserProfile(supabase, user.id);
|
||||
const { data: profileData, error } = await getUserProfile(supabase, uid);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
44
src/app/api/dealApi.ts
Normal file
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";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { SubmitHandler } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DualOptionSelector } from "@/components/dualSelector";
|
||||
import { MultipleOptionSelector } from "@/components/multipleSelector";
|
||||
import BusinessForm from "@/components/BusinessForm";
|
||||
import { businessFormSchema } from "@/types/schemas/application.schema";
|
||||
import Swal from "sweetalert2";
|
||||
import { getCurrentUserID } from "@/app/api/userApi";
|
||||
import { uploadFile } from "@/app/api/generalApi";
|
||||
import { Loader } from "@/components/loading/loader";
|
||||
|
||||
export default function Apply() {
|
||||
const [industry, setIndustry] = useState<string[]>([]);
|
||||
const [isInUS, setIsInUS] = useState("");
|
||||
const [isForSale, setIsForSale] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState("");
|
||||
const [businessPitch, setBusinessPitch] = useState("text");
|
||||
const [projectType, setProjectType] = useState<string[]>([]);
|
||||
const [projectPitch, setProjectPitch] = useState("text");
|
||||
type businessSchema = z.infer<typeof businessFormSchema>;
|
||||
const BUCKET_PITCH_NAME = "business-application";
|
||||
let supabase = createSupabaseClient();
|
||||
|
||||
export default function ApplyBusiness() {
|
||||
const [applyProject, setApplyProject] = useState(false);
|
||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
||||
const [businessPitchFile, setBusinessPitchFile] = useState("");
|
||||
const [projectPitchFile, setProjectPitchFile] = useState("");
|
||||
const MAX_FILE_SIZE = 5000000;
|
||||
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png"];
|
||||
const createPitchDeckSchema = (inputType: string) => {
|
||||
if (inputType === "text") {
|
||||
return z
|
||||
.string()
|
||||
.url("Pitch deck must be a valid URL.")
|
||||
.refine((url) => url.endsWith(".md"), {
|
||||
message: "Pitch deck URL must link to a markdown file (.md).",
|
||||
});
|
||||
} else if (inputType === "file") {
|
||||
return z
|
||||
.custom<File>(
|
||||
(val) => {
|
||||
// confirm val is a File object
|
||||
return val instanceof File; // Ensure it is a File instance
|
||||
},
|
||||
{
|
||||
message: "Input must be a file.",
|
||||
}
|
||||
)
|
||||
.refine((file) => file.size < MAX_FILE_SIZE, {
|
||||
message: "File can't be bigger than 5MB.",
|
||||
})
|
||||
.refine((file) => file.name.endsWith(".md"), {
|
||||
message: "File must be a markdown file (.md).",
|
||||
});
|
||||
} else {
|
||||
return z.any(); // avoid undefined
|
||||
}
|
||||
const alertShownRef = useRef(false);
|
||||
const [success, setSucess] = useState(false);
|
||||
|
||||
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
|
||||
const transformedData = await transformChoice(data);
|
||||
await sendApplication(transformedData);
|
||||
};
|
||||
const imageSchema = z
|
||||
.custom<File>((val) => val && typeof val === "object" && "size" in val && "type" in val, {
|
||||
message: "Input must be a file.",
|
||||
})
|
||||
.refine((file) => file.size < MAX_FILE_SIZE, {
|
||||
message: "File can't be bigger than 5MB.",
|
||||
})
|
||||
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
||||
message: "File format must be either jpg, jpeg, or png.",
|
||||
});
|
||||
const sendApplication = async (recvData: any) => {
|
||||
setSucess(false);
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const pitchType = typeof recvData["businessPitchDeck"];
|
||||
if (pitchType === "object") {
|
||||
if (user?.id) {
|
||||
const uploadSuccess = await uploadFile(
|
||||
recvData["businessPitchDeck"],
|
||||
BUCKET_PITCH_NAME,
|
||||
// file structure: userId/fileName
|
||||
`${user?.id}/pitch-file/pitch.md`
|
||||
);
|
||||
|
||||
const projectFormSchema = z.object({
|
||||
projectName: z.string().min(5, {
|
||||
message: "Project name must be at least 5 characters.",
|
||||
}),
|
||||
projectType: z.string({
|
||||
required_error: "Please select one of the option",
|
||||
}),
|
||||
shortDescription: z
|
||||
.string({
|
||||
required_error: "Please provide a brief description for your project",
|
||||
})
|
||||
.min(10, {
|
||||
message: "Short description must be at least 10 characters.",
|
||||
}),
|
||||
projectPitchDeck: createPitchDeckSchema(projectPitch),
|
||||
projectLogo: imageSchema,
|
||||
|
||||
projectPhotos: z.custom(
|
||||
(value) => {
|
||||
console.log("Tozod", value);
|
||||
if (value instanceof FileList || Array.isArray(value)) {
|
||||
if (value.length === 1) {
|
||||
return false;
|
||||
}
|
||||
return Array.from(value).every((item) => item instanceof File);
|
||||
if (!uploadSuccess) {
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ message: "Must be a FileList or an array of File objects with at least one file." }
|
||||
),
|
||||
minInvest: z
|
||||
.number({
|
||||
required_error: "Minimum invesment must be a number.",
|
||||
invalid_type_error: "Minimum invesment must be a valid number.",
|
||||
})
|
||||
.positive()
|
||||
.max(9999999999, "Minimum invesment must be a realistic amount."),
|
||||
targetInvest: z
|
||||
.number({
|
||||
required_error: "Target invesment must be a number.",
|
||||
invalid_type_error: "Target invesment must be a valid number.",
|
||||
})
|
||||
.positive()
|
||||
.max(9999999999, "Target invesment must be a realistic amount."),
|
||||
deadline: z
|
||||
.string()
|
||||
.min(1, "Deadline is required.")
|
||||
.refine((value) => !isNaN(Date.parse(value)), {
|
||||
message: "Invalid date-time format.",
|
||||
})
|
||||
.transform((value) => new Date(value))
|
||||
.refine((date) => date > new Date(), {
|
||||
message: "Deadline must be in the future.",
|
||||
}),
|
||||
});
|
||||
const businessFormSchema = z.object({
|
||||
companyName: z.string().min(5, {
|
||||
message: "Company name must be at least 5 characters.",
|
||||
}),
|
||||
industry: z.string({
|
||||
required_error: "Please select one of the option",
|
||||
}),
|
||||
isInUS: z
|
||||
.string({
|
||||
required_error: "Please select either 'Yes' or 'No'.",
|
||||
})
|
||||
.transform((val) => val.toLowerCase())
|
||||
.refine((val) => val === "yes" || val === "no", {
|
||||
message: "Please select either 'Yes' or 'No'.",
|
||||
}),
|
||||
isForSale: z
|
||||
.string({
|
||||
required_error: "Please select either 'Yes' or 'No'.",
|
||||
})
|
||||
.transform((val) => val.toLowerCase())
|
||||
.refine((val) => val === "yes" || val === "no", {
|
||||
message: "Please select either 'Yes' or 'No'.",
|
||||
}),
|
||||
isGenerating: z
|
||||
.string({
|
||||
required_error: "Please select either 'Yes' or 'No'.",
|
||||
})
|
||||
.transform((val) => val.toLowerCase())
|
||||
.refine((val) => val === "yes" || val === "no", {
|
||||
message: "Please select either 'Yes' or 'No'.",
|
||||
}),
|
||||
totalRaised: z
|
||||
.number({
|
||||
required_error: "Total raised must be a number.",
|
||||
invalid_type_error: "Total raised must be a valid number.",
|
||||
})
|
||||
.positive()
|
||||
.max(9999999999, "Total raised must be a realistic amount."),
|
||||
communitySize: z.string({
|
||||
required_error: "Please select one of the option",
|
||||
}),
|
||||
businessPitchDeck: createPitchDeckSchema(businessPitch),
|
||||
});
|
||||
let supabase = createSupabaseClient();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue: setValueBusiness,
|
||||
formState: { errors: errorsBusiness },
|
||||
} = useForm({
|
||||
resolver: zodResolver(businessFormSchema),
|
||||
});
|
||||
const {
|
||||
register: registerSecondForm,
|
||||
handleSubmit: handleSecondSubmit,
|
||||
formState: { errors: errorsProject },
|
||||
setValue: setValueProject,
|
||||
} = useForm({
|
||||
resolver: zodResolver(projectFormSchema),
|
||||
});
|
||||
|
||||
const communitySize = ["N/A", "0-5K", "5-10K", "10-20K", "20-50K", "50-100K", "100K+"];
|
||||
|
||||
useEffect(() => {
|
||||
register("industry");
|
||||
register("isInUS");
|
||||
register("isForSale");
|
||||
register("isGenerating");
|
||||
}, [register]);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files) {
|
||||
const filesArray = Array.from(event.target.files);
|
||||
console.log("first file", filesArray);
|
||||
setSelectedImages((prevImages) => {
|
||||
const updatedImages = [...prevImages, ...filesArray];
|
||||
console.log("Updated Images Array:", updatedImages);
|
||||
// ensure we're setting an array of File objects
|
||||
setValueProject("projectPhotos", updatedImages);
|
||||
return updatedImages;
|
||||
});
|
||||
console.log("file upload successful");
|
||||
} else {
|
||||
console.error("user ID is undefined.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
setSelectedImages((prevImages) => {
|
||||
const updatedImages = prevImages.filter((_, i) => i !== index);
|
||||
console.log("After removal - Updated Images:", updatedImages);
|
||||
// ensure we're setting an array of File objects
|
||||
setValueProject("projectPhotos", updatedImages);
|
||||
return updatedImages;
|
||||
const { data, error } = await supabase
|
||||
.from("business_application")
|
||||
.insert([
|
||||
{
|
||||
user_id: user?.id,
|
||||
business_name: recvData["companyName"],
|
||||
business_type_id: recvData["industry"],
|
||||
location: recvData["country"],
|
||||
is_for_sale: recvData["isForSale"],
|
||||
is_generating_revenue: recvData["isGenerating"],
|
||||
is_in_us: recvData["isInUS"],
|
||||
pitch_deck_url:
|
||||
pitchType === "string" ? recvData["businessPitchDeck"] : "",
|
||||
money_raised_to_date: recvData["totalRaised"],
|
||||
community_size: recvData["communitySize"],
|
||||
},
|
||||
])
|
||||
.select();
|
||||
setSucess(true);
|
||||
// console.table(data);
|
||||
Swal.fire({
|
||||
icon: error == null ? "success" : "error",
|
||||
title: error == null ? "success" : "Error: " + error.code,
|
||||
text:
|
||||
error == null ? "Your application has been submitted" : error.message,
|
||||
confirmButtonColor: error == null ? "green" : "red",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed && applyProject) {
|
||||
window.location.href = "/project/apply";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ensureArrayValue = (value: any): File[] => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value instanceof File) return [value];
|
||||
return [];
|
||||
const hasUserApplied = async (userID: string) => {
|
||||
let { data: business, error } = await supabase
|
||||
.from("business")
|
||||
.select("*")
|
||||
.eq("user_id", userID);
|
||||
console.table(business);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
if (business) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const transformChoice = (data: any) => {
|
||||
// convert any yes and no to true or false
|
||||
const transformedData = Object.entries(data).reduce((acc: Record<any, any>, [key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
const lowerValue = value.toLowerCase();
|
||||
if (lowerValue === "yes") {
|
||||
acc[key] = true;
|
||||
} else if (lowerValue === "no") {
|
||||
acc[key] = false;
|
||||
const transformedData = Object.entries(data).reduce(
|
||||
(acc: Record<any, any>, [key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
const lowerValue = value.toLowerCase();
|
||||
if (lowerValue === "yes") {
|
||||
acc[key] = true;
|
||||
} else if (lowerValue === "no") {
|
||||
acc[key] = false;
|
||||
} else {
|
||||
acc[key] = value; // keep other string values unchanged
|
||||
}
|
||||
} else {
|
||||
acc[key] = value; // keep other string values unchanged
|
||||
acc[key] = value; // keep other types unchanged
|
||||
}
|
||||
} else {
|
||||
acc[key] = value; // keep other types unchanged
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return transformedData;
|
||||
};
|
||||
const handleBusinessPitchChange = (type: string) => {
|
||||
setBusinessPitch(type);
|
||||
// clear out old data
|
||||
setValueBusiness("pitchDeck", "");
|
||||
};
|
||||
|
||||
const handleBusinessFieldChange = (fieldName: string, value: any) => {
|
||||
switch (fieldName) {
|
||||
case "isInUS":
|
||||
setIsInUS(value);
|
||||
break;
|
||||
case "isForSale":
|
||||
setIsForSale(value);
|
||||
break;
|
||||
case "isGenerating":
|
||||
setIsGenerating(value);
|
||||
break;
|
||||
}
|
||||
setValueBusiness(fieldName, value);
|
||||
};
|
||||
const handleProjectFieldChange = (fieldName: string, value: any) => {
|
||||
switch (fieldName) {
|
||||
}
|
||||
setValueProject(fieldName, value);
|
||||
};
|
||||
|
||||
const fetchIndustry = async () => {
|
||||
let { data: BusinessType, error } = await supabase.from("business_type").select("value");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
if (BusinessType) {
|
||||
// console.table();
|
||||
setIndustry(BusinessType.map((item) => item.value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitSingleForm = (data: any) => {
|
||||
const pitchDeckSchema = createPitchDeckSchema(businessPitch);
|
||||
pitchDeckSchema.parse(data.businessPitchDeck);
|
||||
console.log("Valid form input:", data);
|
||||
alert(JSON.stringify(data));
|
||||
};
|
||||
|
||||
const onSubmitBothForms = (firstFormData: any, secondFormData: any) => {
|
||||
const formattedSecondFormData = {
|
||||
...secondFormData,
|
||||
projectPhotos: ensureArrayValue(secondFormData.projectPhotos),
|
||||
};
|
||||
alert(JSON.stringify(firstFormData));
|
||||
alert(JSON.stringify(formattedSecondFormData));
|
||||
console.log("Both forms submitted:", {
|
||||
firstFormData,
|
||||
formattedSecondFormData,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitForms = (firstFormData: any) => {
|
||||
const transformedData = transformChoice(firstFormData);
|
||||
if (applyProject) {
|
||||
handleSecondSubmit((secondFormData: any) => {
|
||||
onSubmitBothForms(transformedData, secondFormData);
|
||||
})();
|
||||
} else {
|
||||
onSubmitSingleForm(transformedData);
|
||||
}
|
||||
};
|
||||
const fetchProjectType = async () => {
|
||||
let { data: ProjectType, error } = await supabase.from("project_type").select("value");
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
if (ProjectType) {
|
||||
console.table(ProjectType);
|
||||
setProjectType(ProjectType.map((item) => item.value));
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchIndustry();
|
||||
fetchProjectType();
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
setSucess(false);
|
||||
const userID = await getCurrentUserID();
|
||||
if (userID) {
|
||||
const hasApplied = await hasUserApplied(userID);
|
||||
setSucess(true);
|
||||
if (hasApplied && !alertShownRef.current) {
|
||||
alertShownRef.current = true;
|
||||
Swal.fire({
|
||||
icon: "info",
|
||||
title: "You Already Have an Account",
|
||||
text: "You have already submitted your business application.",
|
||||
confirmButtonText: "OK",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
}
|
||||
setSucess(false);
|
||||
} else {
|
||||
console.error("User ID is undefined.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user ID:", error);
|
||||
}
|
||||
};
|
||||
// setSucess(true);
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Loader isSuccess={success} />
|
||||
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-100 dark:bg-gray-800 p-5">
|
||||
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
|
||||
Apply to raise on B2DVentures
|
||||
</h1>
|
||||
<div className="mt-5 justify-self-center">
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
All information submitted in this application is for internal use only and is treated with the utmost{" "}
|
||||
All information submitted in this application is for internal use
|
||||
only and is treated with the utmost{" "}
|
||||
</p>
|
||||
<p className="text-sm md:text-base text-neutral-500">
|
||||
confidentiality. Companies may apply to raise with B2DVentures more than once.
|
||||
confidentiality. Companies may apply to raise with B2DVentures more
|
||||
than once.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* form */}
|
||||
<form action="" onSubmit={handleSubmit(handleSubmitForms)}>
|
||||
<div className="grid grid-flow-row auto-rows-max w-3/4 ml-1/2 lg:ml-[10%]">
|
||||
<h1 className="text-3xl font-bold mt-10 ml-96">About your company</h1>
|
||||
<p className="ml-96 mt-5 text-neutral-500">
|
||||
<span className="text-red-500 font-bold">**</span>All requested information in this section is required.
|
||||
</p>
|
||||
|
||||
{/* company name */}
|
||||
<div className="ml-96 mt-5 space-y-10">
|
||||
<div className="mt-10 space-y-5">
|
||||
<Label htmlFor="companyName" className="font-bold text-lg">
|
||||
Company name
|
||||
</Label>
|
||||
<div className="flex space-x-5">
|
||||
<Input type="text" id="companyName" className="w-96" {...register("companyName")} />
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
This should be the name your company uses on your <br />
|
||||
website and in the market.
|
||||
</span>
|
||||
</div>
|
||||
{errorsBusiness.companyName && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errorsBusiness.companyName && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.companyName.message as string}</p>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* industry */}
|
||||
|
||||
<MultipleOptionSelector
|
||||
header={<>Industry</>}
|
||||
fieldName="industry"
|
||||
choices={industry}
|
||||
handleFunction={handleBusinessFieldChange}
|
||||
description={<>Choose the industry that best aligns with your business.</>}
|
||||
placeholder="Select an industry"
|
||||
selectLabel="Industry"
|
||||
/>
|
||||
{errorsBusiness.industry && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.industry.message as string}</p>
|
||||
)}
|
||||
{/* How much money has your company raised to date? */}
|
||||
<div className="space-y-5">
|
||||
<Label htmlFor="totalRaised" className="font-bold text-lg">
|
||||
How much money has your company <br /> raised to date?
|
||||
</Label>
|
||||
<div className="flex space-x-5">
|
||||
<Input
|
||||
type="number"
|
||||
id="totalRaised"
|
||||
className="w-96"
|
||||
placeholder="$ 1,000,000"
|
||||
{...register("totalRaised", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
The sum total of past financing, including angel or venture <br />
|
||||
capital, loans, grants, or token sales.
|
||||
</span>
|
||||
</div>
|
||||
{errorsBusiness.totalRaised && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.totalRaised.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Is your company incorporated in the United States? */}
|
||||
<DualOptionSelector
|
||||
label={
|
||||
<>
|
||||
Is your company incorporated in the <br />
|
||||
United States?
|
||||
</>
|
||||
}
|
||||
name="isInUS"
|
||||
choice1="Yes"
|
||||
choice2="No"
|
||||
handleFunction={handleBusinessFieldChange}
|
||||
description={
|
||||
<>
|
||||
Only companies that are incorporated or formed in the US are <br />
|
||||
eligible to raise via Reg CF. If your company is incorporated <br />
|
||||
outside the US, we still encourage you to apply.
|
||||
</>
|
||||
}
|
||||
value={isInUS}
|
||||
/>
|
||||
{errorsBusiness.isInUS && <p className="text-red-500 text-sm">{errorsBusiness.isInUS.message as string}</p>}
|
||||
|
||||
{/* Is your product available (for sale) in market? */}
|
||||
<DualOptionSelector
|
||||
label={
|
||||
<>
|
||||
Is your product available (for sale) <br />
|
||||
in market?
|
||||
</>
|
||||
}
|
||||
name="isForSale"
|
||||
choice1="Yes"
|
||||
choice2="No"
|
||||
handleFunction={handleBusinessFieldChange}
|
||||
description={
|
||||
<>
|
||||
Only check this box if customers can access, use, or buy your <br />
|
||||
product today.
|
||||
</>
|
||||
}
|
||||
value={isForSale}
|
||||
/>
|
||||
{errorsBusiness.isForSale && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.isForSale.message as string}</p>
|
||||
)}
|
||||
|
||||
{/* Is your company generating revenue?*/}
|
||||
<DualOptionSelector
|
||||
label={<>Is your company generating revenue?</>}
|
||||
name="isGenerating"
|
||||
choice1="Yes"
|
||||
choice2="No"
|
||||
handleFunction={handleBusinessFieldChange}
|
||||
description={
|
||||
<>
|
||||
Only check this box if your company is making money. <br />
|
||||
Please elaborate on revenue and other traction below.
|
||||
</>
|
||||
}
|
||||
value={isGenerating}
|
||||
/>
|
||||
{errorsBusiness.isGenerating && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.isGenerating.message as string}</p>
|
||||
)}
|
||||
{/* Pitch deck */}
|
||||
<div className="space-y-5">
|
||||
<Label htmlFor="pitchDeck" className="font-bold text-lg">
|
||||
Pitch deck
|
||||
</Label>
|
||||
<div className="flex space-x-2 w-96">
|
||||
<Button
|
||||
type="button"
|
||||
variant={businessPitch === "text" ? "default" : "outline"}
|
||||
onClick={() => handleBusinessPitchChange("text")}
|
||||
className="w-32 h-12 text-base">
|
||||
Paste URL
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={businessPitch === "file" ? "default" : "outline"}
|
||||
onClick={() => handleBusinessPitchChange("file")}
|
||||
className="w-32 h-12 text-base">
|
||||
Upload a file
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex space-x-5">
|
||||
<Input
|
||||
type={businessPitch === "file" ? "file" : "text"}
|
||||
id="pitchDeck"
|
||||
className="w-96"
|
||||
placeholder={businessPitch === "file" ? "Upload your Markdown file" : "https:// "}
|
||||
accept={businessPitch === "file" ? ".md" : undefined}
|
||||
// if text use normal register
|
||||
{...(businessPitch === "text"
|
||||
? register("businessPitchDeck", { required: true })
|
||||
: {
|
||||
onChange: (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
setValueBusiness("businessPitchDeck", file);
|
||||
setBusinessPitchFile(file?.name || "");
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<span className="text-[12px] text-neutral-500 self-center">
|
||||
Your pitch deck and other application info will be used for <br />
|
||||
internal purposes only. <br />
|
||||
Please make sure this document is publicly accessible. This can <br />
|
||||
be a DocSend, Box, Dropbox, Google Drive or other link.
|
||||
<br />
|
||||
<p className="text-red-500">** support only markdown(.md) format</p>
|
||||
</span>
|
||||
</div>
|
||||
{/* box to show file name */}
|
||||
{businessPitchFile && (
|
||||
<div className="flex justify-between items-center border p-2 rounded w-96 text-sm text-foreground">
|
||||
<span>1. {businessPitchFile}</span>
|
||||
<Button
|
||||
className="ml-4"
|
||||
onClick={() => {
|
||||
setValueBusiness("businessPitchDeck", null);
|
||||
setBusinessPitchFile("");
|
||||
}}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorsBusiness.businessPitchDeck && (
|
||||
<p className="text-red-500 text-sm">{errorsBusiness.businessPitchDeck.message as string}</p>
|
||||
)}
|
||||
<MultipleOptionSelector
|
||||
header={
|
||||
<>
|
||||
What's the rough size of your <br /> community?
|
||||
</>
|
||||
}
|
||||
fieldName="communitySize"
|
||||
choices={communitySize}
|
||||
handleFunction={handleBusinessFieldChange}
|
||||
description={
|
||||
<>
|
||||
{" "}
|
||||
Include your email list, social media following (i.e. Instagram, <br /> Discord, Facebook, Twitter,
|
||||
TikTok). 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>
|
||||
{/* <form action="" onSubmit={handleSubmit(handleSubmitForms)}> */}
|
||||
<BusinessForm
|
||||
onSubmit={onSubmit}
|
||||
applyProject={applyProject}
|
||||
setApplyProject={setApplyProject}
|
||||
/>
|
||||
</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 { useState } from "react";
|
||||
|
||||
import { useDealList } from "./hook";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [graphType, setGraphType] = useState("line");
|
||||
const dealList = useDealList();
|
||||
const totalDealAmount = dealList?.reduce((sum, deal) => sum + deal.deal_amount, 0) || 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{dealList?.map((deal, index) => (
|
||||
<div key={index} className="deal-item">
|
||||
<p>Deal Amount: {deal.deal_amount}</p>
|
||||
<p>Created Time: {new Date(deal.created_time).toUTCString()}</p>
|
||||
<p>Investor ID: {deal.investor_id}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="md:hidden">
|
||||
<Image
|
||||
src="/examples/dashboard-light.png"
|
||||
@ -35,7 +47,7 @@ export default function Dashboard() {
|
||||
<div className="hidden flex-col md:flex">
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Business Dashboard</h2>
|
||||
</div>
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
@ -63,10 +75,10 @@ export default function Dashboard() {
|
||||
</svg>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$45,231.89</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<div className="text-2xl font-bold">${totalDealAmount}</div>
|
||||
{/* <p className="text-xs text-muted-foreground">
|
||||
+20.1% from last month
|
||||
</p>
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@ -90,9 +102,9 @@ export default function Dashboard() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+2350</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{/* <p className="text-xs text-muted-foreground">
|
||||
+180.1% from last month
|
||||
</p>
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@ -117,9 +129,9 @@ export default function Dashboard() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+12,234</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{/* <p className="text-xs text-muted-foreground">
|
||||
+19% from last month
|
||||
</p>
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* <Card>
|
||||
@ -181,11 +193,12 @@ export default function Dashboard() {
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Funds</CardTitle>
|
||||
<CardDescription>
|
||||
You made 265 sales this month.
|
||||
You made {dealList?.length || 0} sales this month.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecentFunds />
|
||||
<RecentFunds>
|
||||
</RecentFunds>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,39 +1,63 @@
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ProjectCard } from "@/components/projectCard";
|
||||
import { getTopProjects } from "@/lib/data/projectQuery";
|
||||
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
|
||||
import { Suspense } from "react";
|
||||
import { FC } from "react";
|
||||
|
||||
const TopProjects = async () => {
|
||||
const supabase = createSupabaseClient();
|
||||
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
|
||||
interface Project {
|
||||
id: number;
|
||||
project_name: string;
|
||||
project_short_description: string;
|
||||
card_image_url: string;
|
||||
published_time: string;
|
||||
business: { location: string }[];
|
||||
project_tag: { tag: { id: number; value: string }[] }[];
|
||||
project_investment_detail: {
|
||||
min_investment: number;
|
||||
total_investment: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
if (topProjectsError) {
|
||||
return <div>Error loading top projects: {topProjectsError}</div>;
|
||||
}
|
||||
interface TopProjectsProps {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
if (!topProjectsData || topProjectsData.length === 0) {
|
||||
const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
|
||||
if (!projects || projects.length === 0) {
|
||||
return <div>No top projects available.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{topProjectsData.map((project) => (
|
||||
{projects.map((project) => (
|
||||
<Link href={`/deals/${project.id}`} key={project.id}>
|
||||
<ProjectCard
|
||||
name={project.project_name}
|
||||
description={project.project_short_description}
|
||||
imageUri={project.card_image_url}
|
||||
joinDate={new Date(project.published_time).toLocaleDateString()}
|
||||
location={project.business.location}
|
||||
tags={project.item_tag.map((item) => item.tag.value)}
|
||||
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
|
||||
location={project.business[0]?.location || ""}
|
||||
tags={project.project_tag.flatMap(
|
||||
(item: { tag: { id: number; value: string }[] }) =>
|
||||
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
|
||||
)}
|
||||
minInvestment={
|
||||
project.project_investment_detail[0]?.min_investment || 0
|
||||
}
|
||||
totalInvestor={0}
|
||||
totalRaised={project.project_investment_detail[0]?.total_investment || 0}
|
||||
totalRaised={
|
||||
project.project_investment_detail[0]?.total_investment || 0
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
@ -44,12 +68,18 @@ const TopProjects = async () => {
|
||||
const ProjectsLoader = () => (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, index) => (
|
||||
<div key={index} className="h-64 bg-gray-200 animate-pulse rounded-lg"></div>
|
||||
<div
|
||||
key={index}
|
||||
className="h-64 bg-gray-200 animate-pulse rounded-lg"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default async function Home() {
|
||||
const supabase = createSupabaseClient();
|
||||
const { data: topProjectsData, error: topProjectsError } =
|
||||
await getTopProjects(supabase);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="relative mx-auto">
|
||||
@ -57,9 +87,14 @@ export default async function Home() {
|
||||
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
|
||||
<div className="container max-w-screen-xl flex flex-col">
|
||||
<span className="mx-20 px-10 py-10">
|
||||
<p className="text-4xl font-bold">Explore the world of ventures</p>
|
||||
<p className="text-4xl font-bold">
|
||||
Explore the world of ventures
|
||||
</p>
|
||||
<span className="text-lg">
|
||||
<p>Unlock opportunities and connect with a community of passionate</p>
|
||||
<p>
|
||||
Unlock opportunities and connect with a community of
|
||||
passionate
|
||||
</p>
|
||||
<p>investors and innovators.</p>
|
||||
<p>Together, we turn ideas into impact.</p>
|
||||
</span>
|
||||
@ -107,11 +142,23 @@ export default async function Home() {
|
||||
</CardHeader>
|
||||
<CardContent className="flex gap-2">
|
||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||
<Image
|
||||
src={"/github.svg"}
|
||||
width={20}
|
||||
height={20}
|
||||
alt="github"
|
||||
className="scale-75 md:scale-100"
|
||||
/>
|
||||
Github
|
||||
</Button>
|
||||
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
|
||||
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
|
||||
<Image
|
||||
src={"/github.svg"}
|
||||
width={20}
|
||||
height={20}
|
||||
alt="github"
|
||||
className="scale-75 md:scale-100"
|
||||
/>
|
||||
Github
|
||||
</Button>
|
||||
</CardContent>
|
||||
@ -123,10 +170,12 @@ export default async function Home() {
|
||||
<div className="flex flex-col px-10">
|
||||
<span className="pb-5">
|
||||
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
|
||||
<p className="text-md md:text-lg">The deals attracting the most interest right now</p>
|
||||
<p className="text-md md:text-lg">
|
||||
The deals attracting the most interest right now
|
||||
</p>
|
||||
</span>
|
||||
<Suspense fallback={<ProjectsLoader />}>
|
||||
<TopProjects />
|
||||
<TopProjects projects={topProjectsData || []} />
|
||||
</Suspense>
|
||||
<div className="self-center py-5 scale-75 md:scale-100">
|
||||
<Button>
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 mt-10">
|
||||
<Label htmlFor={props.name} className="font-bold text-lg">
|
||||
{props.label}
|
||||
</Label>
|
||||
@ -23,7 +23,7 @@ export function DualOptionSelector(props: SelectorInterface) {
|
||||
<Button
|
||||
type="button"
|
||||
variant={props.value === props.choice1 ? "default" : "outline"}
|
||||
onClick={() => props.handleFunction(props.name, props.choice1)}
|
||||
onClick={() => props.handleFunction(props.choice1)}
|
||||
className="w-20 h-12 text-base"
|
||||
>
|
||||
{props.choice1}
|
||||
@ -31,7 +31,7 @@ export function DualOptionSelector(props: SelectorInterface) {
|
||||
<Button
|
||||
type="button"
|
||||
variant={props.value === props.choice2 ? "default" : "outline"}
|
||||
onClick={() => props.handleFunction(props.name, props.choice2)}
|
||||
onClick={() => props.handleFunction(props.choice2)}
|
||||
className="w-20 h-12 text-base"
|
||||
>
|
||||
{props.choice2}
|
||||
|
||||
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,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ReactElement } from "react";
|
||||
import { ReactElement, useState } from "react";
|
||||
|
||||
interface MultipleOptionSelector {
|
||||
interface MultipleOptionSelectorProps {
|
||||
header: ReactElement;
|
||||
fieldName: string;
|
||||
choices: string[];
|
||||
handleFunction: Function;
|
||||
choices: { id: number; name: string }[];
|
||||
handleFunction: Function | null;
|
||||
description: ReactElement;
|
||||
placeholder: string;
|
||||
selectLabel: string;
|
||||
}
|
||||
|
||||
export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
||||
export function MultipleOptionSelector(props: MultipleOptionSelectorProps) {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<div className="mt-10 space-y-5">
|
||||
<Label htmlFor={props.fieldName} className="font-bold text-lg mt-10">
|
||||
@ -28,9 +29,15 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
||||
</Label>
|
||||
<div className="flex space-x-5">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
props.handleFunction(props.fieldName, value);
|
||||
// console.log(value, props.fieldName);
|
||||
value={value}
|
||||
onValueChange={(id) => {
|
||||
setValue(id);
|
||||
const selectedChoice = props.choices.find(
|
||||
(choice) => choice.id.toString() === id
|
||||
);
|
||||
if (selectedChoice && props.handleFunction) {
|
||||
props.handleFunction(selectedChoice);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-96">
|
||||
@ -40,8 +47,8 @@ export function MultipleOptionSelector(props: MultipleOptionSelector) {
|
||||
<SelectGroup>
|
||||
<SelectLabel>{props.selectLabel}</SelectLabel>
|
||||
{props.choices.map((i) => (
|
||||
<SelectItem key={i} value={i}>
|
||||
{i}
|
||||
<SelectItem key={i.id} value={i.id.toString()}>
|
||||
{i.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
|
||||
@ -51,8 +51,8 @@ export function NavigationBar() {
|
||||
const projectComponents = [
|
||||
{
|
||||
title: "Projects",
|
||||
href: "/landing",
|
||||
description: "Raise on B2DVentures",
|
||||
href: "/project/apply",
|
||||
description: "Start your new project on B2DVentures",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ const UnAuthenticatedComponents = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AuthenticatedComponents = () => {
|
||||
const AuthenticatedComponents = ({ uid }: { uid: string }) => {
|
||||
let notifications = 100;
|
||||
const displayValue = notifications >= 100 ? "..." : notifications;
|
||||
return (
|
||||
@ -58,7 +58,7 @@ const AuthenticatedComponents = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link href="/profile">Profile</Link>
|
||||
<Link href={`/profile/${uid}`}>Profile</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
@ -88,7 +88,7 @@ export function ProfileBar() {
|
||||
<>
|
||||
{sessionLoaded ? (
|
||||
user ? (
|
||||
<AuthenticatedComponents />
|
||||
<AuthenticatedComponents uid={user.id} />
|
||||
) : (
|
||||
<UnAuthenticatedComponents />
|
||||
)
|
||||
|
||||
@ -25,7 +25,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px]",
|
||||
"flex flex-col group border-[1px] border-border relative hover:shadow-md rounded-xl h-[450px] ",
|
||||
props.className
|
||||
)}>
|
||||
<div className="flex flex-col h-full">
|
||||
@ -58,7 +58,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
||||
|
||||
{/* Info 1 */}
|
||||
<div>
|
||||
<div className="transition-transform duration-500 transform opacity-100 group-hover:opacity-0 p-4">
|
||||
<div className="transition-transform duration-500 transform opacity-100 group-hover:opacity-0 p-4 ">
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarDaysIcon width={20} />
|
||||
@ -79,7 +79,7 @@ export function ProjectCard(props: ProjectCardProps) {
|
||||
</div>
|
||||
|
||||
{/* Info 2 */}
|
||||
<div className="hidden group-hover:flex group-hover:absolute group-hover:bottom-4 p-4">
|
||||
<div className="hidden group-hover:flex group-hover:absolute group-hover:bottom-4 p-4 ">
|
||||
{/* Info 2 (Visible on hover) */}
|
||||
<div className="transition-transform duration-500 transform translate-y-6 opacity-0 group-hover:translate-y-0 group-hover:opacity-100">
|
||||
<hr className="-ml-4 mb-2" />
|
||||
|
||||
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
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(
|
||||
client: SupabaseClient,
|
||||
numberOfRecords: number = 4,
|
||||
numberOfRecords: number = 4
|
||||
) {
|
||||
try {
|
||||
const { data, error } = await client
|
||||
@ -21,7 +21,7 @@ async function getTopProjects(
|
||||
target_investment,
|
||||
investment_deadline
|
||||
),
|
||||
item_tag (
|
||||
project_tag (
|
||||
tag (
|
||||
id,
|
||||
value
|
||||
@ -30,7 +30,7 @@ async function getTopProjects(
|
||||
business (
|
||||
location
|
||||
)
|
||||
`,
|
||||
`
|
||||
)
|
||||
.order("published_time", { ascending: false })
|
||||
.limit(numberOfRecords);
|
||||
@ -48,8 +48,10 @@ async function getTopProjects(
|
||||
}
|
||||
|
||||
function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
||||
return client.from("project").select(
|
||||
`
|
||||
return client
|
||||
.from("project")
|
||||
.select(
|
||||
`
|
||||
project_name,
|
||||
project_short_description,
|
||||
project_description,
|
||||
@ -65,13 +67,17 @@ function getProjectDataQuery(client: SupabaseClient, projectId: number) {
|
||||
tag_name:value
|
||||
)
|
||||
)
|
||||
`,
|
||||
).eq("id", projectId).single();
|
||||
`
|
||||
)
|
||||
.eq("id", projectId)
|
||||
.single();
|
||||
}
|
||||
|
||||
async function getProjectData(client: SupabaseClient, projectId: number) {
|
||||
const query = client.from("project").select(
|
||||
`
|
||||
const query = client
|
||||
.from("project")
|
||||
.select(
|
||||
`
|
||||
project_name,
|
||||
project_short_description,
|
||||
project_description,
|
||||
@ -87,8 +93,10 @@ async function getProjectData(client: SupabaseClient, projectId: number) {
|
||||
tag_name:value
|
||||
)
|
||||
)
|
||||
`,
|
||||
).eq("id", projectId).single();
|
||||
`
|
||||
)
|
||||
.eq("id", projectId)
|
||||
.single();
|
||||
|
||||
const { data, error } = await query;
|
||||
return { data, error };
|
||||
@ -118,19 +126,21 @@ function searchProjectsQuery(
|
||||
sortByTimeFilter,
|
||||
page = 1,
|
||||
pageSize = 4,
|
||||
}: FilterProjectQueryParams,
|
||||
}: FilterProjectQueryParams
|
||||
) {
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize - 1;
|
||||
|
||||
let query = client.from("project").select(
|
||||
`
|
||||
let query = client
|
||||
.from("project")
|
||||
.select(
|
||||
`
|
||||
project_id:id,
|
||||
project_name,
|
||||
published_time,
|
||||
project_short_description,
|
||||
card_image_url,
|
||||
...project_status!project_project_status_id_fkey!inner (
|
||||
...project_status!inner (
|
||||
project_status:value
|
||||
),
|
||||
...project_investment_detail!inner (
|
||||
@ -150,8 +160,10 @@ function searchProjectsQuery(
|
||||
),
|
||||
business_location:location
|
||||
)
|
||||
`,
|
||||
).order("published_time", { ascending: false }).range(start, end);
|
||||
`
|
||||
)
|
||||
.order("published_time", { ascending: false })
|
||||
.range(start, end);
|
||||
|
||||
if (sortByTimeFilter === "all") {
|
||||
sortByTimeFilter = undefined;
|
||||
|
||||
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