Merge branch 'main' into back-end

This commit is contained in:
sirin 2024-10-27 22:59:33 +07:00
commit 40f2557de4
17 changed files with 6833 additions and 1923 deletions

512
package-lock.json generated
View File

@ -9,12 +9,14 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1",
@ -33,6 +35,7 @@
"b2d-ventures": "file:",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.2.0",
@ -44,6 +47,7 @@
"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",
@ -60,6 +64,8 @@
"@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",
@ -935,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",
@ -1333,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",
@ -2311,6 +2380,39 @@
"@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",
@ -2322,6 +2424,15 @@
"@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",
@ -2882,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",
@ -3153,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",
@ -3213,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",
@ -5675,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",
@ -7212,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",

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
@ -17,6 +18,7 @@
"@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",
@ -35,6 +37,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",
@ -46,6 +49,7 @@
"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",
@ -62,6 +66,8 @@
"@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",

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

@ -1,26 +1,53 @@
"use client";
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { SubmitHandler } from "react-hook-form";
import { z } from "zod";
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";
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 alertShownRef = useRef(false);
const [success, setSucess] = useState(false);
const onSubmit: SubmitHandler<businessSchema> = async (data) => {
const transformedData = await transformChoice(data);
console.log(transformedData);
await sendRegistration(transformedData);
await sendApplication(transformedData);
};
const sendRegistration = async (recvData: any) => {
const sendApplication = async (recvData: any) => {
setSucess(false);
const {
data: { user },
} = await supabase.auth.getUser();
// console.log(user?.id);
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`
);
if (!uploadSuccess) {
return;
}
console.log("file upload successful");
} else {
console.error("user ID is undefined.");
return;
}
}
const { data, error } = await supabase
.from("business_application")
@ -33,13 +60,15 @@ export default function ApplyBusiness() {
is_for_sale: recvData["isForSale"],
is_generating_revenue: recvData["isGenerating"],
is_in_us: recvData["isInUS"],
pitch_deck_url: recvData["businessPitchDeck"],
pitch_deck_url:
pitchType === "string" ? recvData["businessPitchDeck"] : "",
money_raised_to_date: recvData["totalRaised"],
community_size: recvData["communitySize"],
},
])
.select();
console.table(data);
setSucess(true);
// console.table(data);
Swal.fire({
icon: error == null ? "success" : "error",
title: error == null ? "success" : "Error: " + error.code,
@ -55,7 +84,20 @@ export default function ApplyBusiness() {
});
};
let supabase = createSupabaseClient();
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(
@ -78,9 +120,44 @@ export default function ApplyBusiness() {
);
return transformedData;
};
useEffect(() => {
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

View File

@ -1,39 +1,63 @@
import Image from "next/image";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { ProjectCard } from "@/components/projectCard";
import { getTopProjects } from "@/lib/data/projectQuery";
import { createSupabaseClient } from "@/lib/supabase/serverComponentClient";
import { Suspense } from "react";
import { FC } from "react";
const TopProjects = async () => {
const supabase = createSupabaseClient();
const { data: topProjectsData, error: topProjectsError } = await getTopProjects(supabase);
interface Project {
id: number;
project_name: string;
project_short_description: string;
card_image_url: string;
published_time: string;
business: { location: string }[];
project_tag: { tag: { id: number; value: string }[] }[];
project_investment_detail: {
min_investment: number;
total_investment: number;
}[];
}
if (topProjectsError) {
return <div>Error loading top projects: {topProjectsError}</div>;
}
interface TopProjectsProps {
projects: Project[];
}
if (!topProjectsData || topProjectsData.length === 0) {
const TopProjects: FC<TopProjectsProps> = ({ projects }) => {
if (!projects || projects.length === 0) {
return <div>No top projects available.</div>;
}
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{topProjectsData.map((project) => (
{projects.map((project) => (
<Link href={`/deals/${project.id}`} key={project.id}>
<ProjectCard
name={project.project_name}
description={project.project_short_description}
imageUri={project.card_image_url}
joinDate={new Date(project.published_time).toLocaleDateString()}
location={project.business.location}
tags={project.item_tag.map((item) => item.tag.value)}
minInvestment={project.project_investment_detail[0]?.min_investment || 0}
location={project.business[0]?.location || ""}
tags={project.project_tag.flatMap(
(item: { tag: { id: number; value: string }[] }) =>
Array.isArray(item.tag) ? item.tag.map((tag) => tag.value) : []
)}
minInvestment={
project.project_investment_detail[0]?.min_investment || 0
}
totalInvestor={0}
totalRaised={project.project_investment_detail[0]?.total_investment || 0}
totalRaised={
project.project_investment_detail[0]?.total_investment || 0
}
/>
</Link>
))}
@ -44,12 +68,18 @@ const TopProjects = async () => {
const ProjectsLoader = () => (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, index) => (
<div key={index} className="h-64 bg-gray-200 animate-pulse rounded-lg"></div>
<div
key={index}
className="h-64 bg-gray-200 animate-pulse rounded-lg"
></div>
))}
</div>
);
export default async function Home() {
const supabase = createSupabaseClient();
const { data: topProjectsData, error: topProjectsError } =
await getTopProjects(supabase);
return (
<main>
<div className="relative mx-auto">
@ -57,9 +87,14 @@ export default async function Home() {
<div className="flex flex-row bg-slate-100 dark:bg-gray-800">
<div className="container max-w-screen-xl flex flex-col">
<span className="mx-20 px-10 py-10">
<p className="text-4xl font-bold">Explore the world of ventures</p>
<p className="text-4xl font-bold">
Explore the world of ventures
</p>
<span className="text-lg">
<p>Unlock opportunities and connect with a community of passionate</p>
<p>
Unlock opportunities and connect with a community of
passionate
</p>
<p>investors and innovators.</p>
<p>Together, we turn ideas into impact.</p>
</span>
@ -107,11 +142,23 @@ export default async function Home() {
</CardHeader>
<CardContent className="flex gap-2">
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
<Image
src={"/github.svg"}
width={20}
height={20}
alt="github"
className="scale-75 md:scale-100"
/>
Github
</Button>
<Button className="flex gap-1 border-2 border-border rounded-md p-1 bg-background text-foreground scale-75 md:scale-100">
<Image src={"/github.svg"} width={20} height={20} alt="github" className="scale-75 md:scale-100" />
<Image
src={"/github.svg"}
width={20}
height={20}
alt="github"
className="scale-75 md:scale-100"
/>
Github
</Button>
</CardContent>
@ -123,10 +170,12 @@ export default async function Home() {
<div className="flex flex-col px-10">
<span className="pb-5">
<p className="text-xl md:text-2xl font-bold">Hottest Deals</p>
<p className="text-md md:text-lg">The deals attracting the most interest right now</p>
<p className="text-md md:text-lg">
The deals attracting the most interest right now
</p>
</span>
<Suspense fallback={<ProjectsLoader />}>
<TopProjects />
<TopProjects projects={topProjectsData || []} />
</Suspense>
<div className="self-center py-5 scale-75 md:scale-100">
<Button>

View File

@ -1,25 +1,280 @@
"use client";
import { useState } from "react";
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>;
export default function ApplyProject() {
const [projectType, setProjectType] = useState<string[]>([]);
const [projectPitch, setProjectPitch] = useState("text");
const [applyProject, setApplyProject] = useState(false);
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [projectPitchFile, setProjectPitchFile] = useState("");
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("มาแน้ววว");
console.table(data);
await sendApplication(data);
// console.table(data);
// console.log(typeof data["projectPhotos"], data["projectPhotos"]);
};
const saveApplicationData = async (recvData: any, userId: string) => {
const pitchType = typeof recvData["projectPitchDeck"];
const { data: projectData, error: projectError } = await supabase
.from("project_application")
.insert([
{
user_id: userId,
pitch_deck_url:
pitchType === "string" ? recvData["projectPitchDeck"] : "",
target_investment: recvData["targetInvest"],
deadline: recvData["deadline"],
project_name: recvData["projectName"],
project_type_id: recvData["projectType"],
short_description: recvData["shortDescription"],
min_investment: recvData["minInvest"],
},
])
.select();
return { projectId: projectData?.[0]?.id, error: projectError };
};
const saveTags = async (tags: string[], projectId: string) => {
const tagPromises = tags.map(async (tag) => {
const response = await supabase
.from("project_application_tag")
.insert([{ tag_id: tag, item_id: projectId }])
.select();
// console.log("Insert response for tag:", tag, response);
return response;
});
const results = await Promise.all(tagPromises);
// Collect errors
const errors = results
.filter((result) => result.error)
.map((result) => result.error);
return { errors };
};
const uploadPitchFile = async (
file: File,
userId: string,
projectId: string
) => {
if (!file || !userId) {
console.error("Pitch file or user ID is undefined.");
return false;
}
return await uploadFile(
file,
BUCKET_PITCH_APPLICATION_NAME,
`${userId}/${projectId}/pitches/${file.name}`
);
};
const uploadLogoAndPhotos = async (
logoFile: File,
photos: File[],
userId: string,
projectId: string
) => {
const uploadResults: { logo?: any; photos: any[] } = { photos: [] };
// upload logo
if (logoFile) {
const logoResult = await uploadFile(
logoFile,
BUCKET_PITCH_APPLICATION_NAME,
`${userId}/${projectId}/logo/${logoFile.name}`
);
if (!logoResult.success) {
console.error("Error uploading logo:", logoResult.errors);
return { success: false, logo: logoResult, photos: [] };
}
uploadResults.logo = logoResult;
}
// upload each photo
const uploadPhotoPromises = photos.map((image) =>
uploadFile(
image,
BUCKET_PITCH_APPLICATION_NAME,
`${userId}/${projectId}/photos/${image.name}`
)
);
const photoResults = await Promise.all(uploadPhotoPromises);
uploadResults.photos = photoResults;
// check if all uploads were successful
const allUploadsSuccessful = photoResults.every((result) => result.success);
return {
success: allUploadsSuccessful,
logo: uploadResults.logo,
photos: uploadResults.photos,
};
};
const displayAlert = (error: any) => {
Swal.fire({
icon: error == null ? "success" : "error",
title: error == null ? "Success" : `Error: ${error.code}`,
text:
error == null ? "Your application has been submitted" : error.message,
confirmButtonColor: error == null ? "green" : "red",
}).then((result) => {
if (result.isConfirmed) {
// window.location.href = "/";
}
});
};
const sendApplication = async (recvData: any) => {
setIsSuccess(false);
const {
data: { user },
} = await supabase.auth.getUser();
if (!user?.id) {
console.error("User ID is undefined.");
return;
}
// save application data
const { projectId, error } = await saveApplicationData(recvData, user.id);
if (error) {
displayAlert(error);
return;
}
const tagError = await saveTags(recvData["tag"], projectId);
// if (tagError) {
// displayAlert(tagError);
// return;
// }
// upload pitch file if its a file
if (typeof recvData["projectPitchDeck"] === "object") {
const uploadPitchSuccess = await uploadPitchFile(
recvData["projectPitchDeck"],
user.id,
projectId
);
if (!uploadPitchSuccess) {
console.error("Error uploading pitch file.");
} else {
console.log("Pitch file uploaded successfully.");
}
}
// upload logo and photos
const { success, logo, photos } = await uploadLogoAndPhotos(
recvData["projectLogo"],
recvData["projectPhotos"],
user.id,
projectId
);
if (!success) {
console.error("Error uploading media files.");
}
// console.log("Bucket Name:", BUCKET_PITCH_APPLICATION_NAME);
// console.log("Logo Path:", logo.data.path);
// console.table(photos);
const logoURL = await getPrivateURL(
logo.data.path,
BUCKET_PITCH_APPLICATION_NAME
);
let photoURLsArray: string[] = [];
const photoURLPromises = photos.map(
async (item: {
success: boolean;
errors: typeof errors;
data: { path: string };
}) => {
const photoURL = await getPrivateURL(
item.data.path,
BUCKET_PITCH_APPLICATION_NAME
);
if (photoURL?.signedUrl) {
photoURLsArray.push(photoURL.signedUrl);
} else {
console.error("Signed URL for photo is undefined.");
}
}
);
await Promise.all(photoURLPromises);
// console.log(logoURL.publicUrl, projectId, logo.data.path);
// console.log(logoURL?.signedUrl, projectId);
// console.log(photoURLsArray[0], photoURLsArray[1]);
if (logoURL?.signedUrl) {
await updateImageURL(logoURL.signedUrl, "project_logo", projectId);
} else {
console.error("Signed URL for logo is undefined.");
}
await updateImageURL(photoURLsArray, "project_photos", projectId);
// console.log(logoURL, photosUrl);
setIsSuccess(true);
displayAlert(error);
};
const updateImageURL = async (
url: string | string[],
columnName: string,
projectId: number
) => {
const { error } = await supabase
.from("project_application")
.update({ [columnName]: url })
.eq("id", projectId);
// console.log(
// `Updating ${columnName} with URL: ${url} for project ID: ${projectId}`
// );
if (error) {
console.error(error);
}
};
const getPrivateURL = async (path: string, bucketName: string) => {
const { data } = await supabase.storage
.from(bucketName)
.createSignedUrl(path, 9999999999999999999999999999);
// console.table(data);
return data;
};
return (
<div>
<div className="grid auto-rows-max w-3/4 ml-48 bg-zinc-100 dark:bg-zinc-900 mt-10 pt-12 pb-12">
<Loader isSuccess={isSuccess} />
<div className="grid grid-flow-row auto-rows-max w-full h-52 md:h-92 bg-gray-2s00 dark:bg-gray-800 p-5">
<h1 className="text-2xl md:text-5xl font-medium md:font-bold justify-self-center md:mt-8">
Apply to raise on B2DVentures
</h1>
<div className="mt-5 justify-self-center">
<p className="text-sm md:text-base text-neutral-500">
Begin Your First Fundraising Project. Starting a fundraising project
is mandatory for all businesses.
</p>
<p className="text-sm md:text-base text-neutral-500">
This step is crucial to begin your journey and unlock the necessary
tools for raising funds.
</p>
</div>
</div>
<div className="grid auto-rows-max bg-zinc-100 dark:bg-zinc-900 pt-12 -mb-6">
<ProjectForm onSubmit={onSubmit} />
</div>
</div>

View File

@ -17,6 +17,21 @@ 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">;
@ -38,6 +53,11 @@ const ProjectForm = ({
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>,
@ -83,8 +103,25 @@ const ProjectForm = ({
}
}
};
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}>
@ -92,23 +129,14 @@ const ProjectForm = ({
onSubmit={form.handleSubmit(onSubmit as SubmitHandler<projectSchema>)}
className="space-y-8"
>
<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>
<div className="ml-96 mt-5 space-y-10">
<div className="ml-96 space-y-10">
{/* project name */}
<FormField
control={form.control}
name="projectName"
render={({ field }: { field: any }) => (
<FormItem>
<div className="mt-10 space-y-5">
<div className="space-y-5">
<FormLabel className="font-bold text-lg">
Project name
</FormLabel>
@ -139,7 +167,7 @@ const ProjectForm = ({
fieldName="projectType"
choices={projectType}
handleFunction={(selectedValues: any) => {
field.onChange(selectedValues.name);
field.onChange(selectedValues.id);
}}
description={
<>Please specify the primary purpose of the funds</>
@ -447,15 +475,117 @@ const ProjectForm = ({
</FormItem>
)}
/>
<center>
<Button
className="mt-12 mb-20 h-10 text-base font-bold py-6 px-5"
type="submit"
>
Submit application
</Button>
</center>
{/* 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>
);

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ const projectFormSchema = z.object({
projectName: z.string().min(5, {
message: "Project name must be at least 5 characters.",
}),
projectType: z.string({
projectType: z.number({
required_error: "Please select one of the option",
}),
shortDescription: z
@ -90,6 +90,10 @@ const projectFormSchema = z.object({
.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({
@ -147,7 +151,7 @@ const businessFormSchema = z.object({
.refine((file) => file.size < MAX_FILE_SIZE, {
message: "File can't be bigger than 5MB.",
})
.refine((file) => file.name.endsWith(".md"), {
.refine((file) => file.name.toLowerCase().endsWith(".md"), {
message: "File must be a markdown file (.md).",
}),
]),