From b3fb95bf37ddd706c0f1b07a2179e2f5bd41bc4c Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 4 Apr 2025 23:14:19 -0700 Subject: [PATCH 01/17] fix: like/follow routes --- backend/projects/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/projects/urls.py b/backend/projects/urls.py index f31094d..3f27e09 100644 --- a/backend/projects/urls.py +++ b/backend/projects/urls.py @@ -5,6 +5,6 @@ path('', get_projects, name='get_projects'), # Fetch all projects path('create/', ProjectCRUDView.as_view(), name='project-create'), # Create project path('/', ProjectCRUDView.as_view(), name='project-detail'), # Read, Update, Delete project by ID - path('like/', toggle_like, name='toggle_like'), - path('follow/', toggle_follow, name='toggle_follow') + path('like/', toggle_like, name='toggle_like'), + path('follow/', toggle_follow, name='toggle_follow') ] From b02100a2b6e478836cfe1a7011b2a3fbdf2a7a25 Mon Sep 17 00:00:00 2001 From: Rebecca Smith <2145912+Rebeccals1@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:22:09 -0700 Subject: [PATCH 02/17] feat: Rebeccals onboard (#43) * Updated Onboarding Page * More onboarding updates * Fixed onboard container being too small. Added link to home slideshow. * changed onboard so its public * chore: workflow double run fix * chore: double unit test run fix --------- Co-authored-by: Rebecca Smith <2145912+Rebeccals@users.noreply.github.com> Co-authored-by: Larry La --- .github/workflows/deployment.yml | 2 +- .github/workflows/django-tests.yml | 2 +- frontend/bitmatch/package-lock.json | 442 ++++++++++++++++++ frontend/bitmatch/package.json | 3 + frontend/bitmatch/src/App.jsx | 127 ++--- .../components/onboarding/CreateProfile.jsx | 126 +++++ .../src/components/onboarding/Interest.jsx | 88 ++++ .../src/components/onboarding/Location.jsx | 97 ++++ .../src/components/onboarding/Roles.jsx | 88 ++++ .../src/components/onboarding/Skills.jsx | 88 ++++ .../components/onboarding/StepIndicator.jsx | 20 + .../src/components/ui/ImageSlideshow.jsx | 35 +- frontend/bitmatch/src/components/ui/badge.jsx | 14 + .../bitmatch/src/components/ui/checkbox.jsx | 29 ++ frontend/bitmatch/src/components/ui/toast.jsx | 85 ++++ .../bitmatch/src/components/ui/toaster.jsx | 33 ++ frontend/bitmatch/src/hooks/use-toast.js | 155 ++++++ frontend/bitmatch/src/views/HomePage.jsx | 2 +- frontend/bitmatch/src/views/OnboardPage.jsx | 242 +++++++--- frontend/bitmatch/src/views/SignInPage.jsx | 2 +- 20 files changed, 1526 insertions(+), 154 deletions(-) create mode 100644 frontend/bitmatch/src/components/onboarding/CreateProfile.jsx create mode 100644 frontend/bitmatch/src/components/onboarding/Interest.jsx create mode 100644 frontend/bitmatch/src/components/onboarding/Location.jsx create mode 100644 frontend/bitmatch/src/components/onboarding/Roles.jsx create mode 100644 frontend/bitmatch/src/components/onboarding/Skills.jsx create mode 100644 frontend/bitmatch/src/components/onboarding/StepIndicator.jsx create mode 100644 frontend/bitmatch/src/components/ui/badge.jsx create mode 100644 frontend/bitmatch/src/components/ui/checkbox.jsx create mode 100644 frontend/bitmatch/src/components/ui/toast.jsx create mode 100644 frontend/bitmatch/src/components/ui/toaster.jsx create mode 100644 frontend/bitmatch/src/hooks/use-toast.js diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b587b23..742c666 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -6,7 +6,7 @@ on: - "*" pull_request: branches: - - "*" + - main jobs: terraform: diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml index 0eebed1..43c15a9 100644 --- a/.github/workflows/django-tests.yml +++ b/.github/workflows/django-tests.yml @@ -6,7 +6,7 @@ on: - "*" pull_request: branches: - - "*" + - main workflow_dispatch: jobs: diff --git a/frontend/bitmatch/package-lock.json b/frontend/bitmatch/package-lock.json index 1b9f647..bc61c46 100644 --- a/frontend/bitmatch/package-lock.json +++ b/frontend/bitmatch/package-lock.json @@ -9,11 +9,14 @@ "version": "0.0.0", "dependencies": { "@clerk/clerk-react": "^5.24.0", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-toggle": "^1.1.2", "axios": "^1.8.3", "class-variance-authority": "^0.7.1", @@ -1168,6 +1171,62 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "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-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "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-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -1527,6 +1586,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", @@ -1544,6 +1646,307 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.7.tgz", + "integrity": "sha512-0IWTbAUKvzdpOaWDMZisXZvScXzF0phaQjWspK8RUMEUxjLbli+886mB/kXTIC3F+t5vQ0n0vYn+dsX8s+WdfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.6", + "@radix-ui/react-portal": "1.1.5", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.1.3" + }, + "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-toast/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz", + "integrity": "sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-slot": "1.2.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-toast/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.6.tgz", + "integrity": "sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "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-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.5.tgz", + "integrity": "sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-toast/node_modules/@radix-ui/react-presence": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", + "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.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-toast/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz", + "integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.3.tgz", + "integrity": "sha512-oXSF3ZQRd5fvomd9hmUCb2EHSZbPp3ZSHAHJJU/DlF9XoFkJBBW8RHU/E8WEH+RbSfJd/QFA0sl8ClJXknBwHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.3" + }, + "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-toggle": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", @@ -1635,6 +2038,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", @@ -1671,6 +2089,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "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/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", @@ -2539,6 +2980,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" }, diff --git a/frontend/bitmatch/package.json b/frontend/bitmatch/package.json index 5d62722..734b044 100644 --- a/frontend/bitmatch/package.json +++ b/frontend/bitmatch/package.json @@ -11,11 +11,14 @@ }, "dependencies": { "@clerk/clerk-react": "^5.24.0", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-toggle": "^1.1.2", "axios": "^1.8.3", "class-variance-authority": "^0.7.1", diff --git a/frontend/bitmatch/src/App.jsx b/frontend/bitmatch/src/App.jsx index 1dbb200..4431275 100644 --- a/frontend/bitmatch/src/App.jsx +++ b/frontend/bitmatch/src/App.jsx @@ -16,86 +16,91 @@ import ProjectDetailPage from "./views/IndividualProjectPage"; import AddProjectPage from "./views/AddProjectPage"; import SignUpPage from "./views/SignUpPage"; import SignInPage from "./views/SignInPage"; + import OnboardPage from "./views/OnboardPage"; -// import BrowsePage from "./views/BrowsePage"; -// import AboutPage from "./views/AboutPage"; +import InterestPage from './components/onboarding/Interest'; +import LocationPage from './components/onboarding/Location'; +import PositionPage from './components/onboarding/Roles'; +import SkillsPage from './components/onboarding/Skills'; +import UserPage from './components/onboarding/CreateProfile'; import "./styles/global.css"; +// AppRoutes is separated for access to hooks function AppRoutes() { const location = useLocation(); const { isSignedIn } = useUser(); - const isLanding = location.pathname === "/"; - const layoutClass = - isLanding && !isSignedIn - ? "py-8" - : "container mx-auto px-4 py-16 flex pb-6 flex-col items-center justify-center min-h-screen"; + + const pathname = location.pathname; + const isLanding = pathname === "/" && !isSignedIn; + const isOnboard = pathname.startsWith("/onboard"); + + const shouldUseContainer = !isLanding && !isOnboard; + + const layoutClass = shouldUseContainer + ? "container mx-auto px-4 py-16 pb-6 min-h-screen" + : ""; return ( -
- - {/* Landing or Home */} - - - - - - - - - } - /> + <> + - {/* Signed-in only routes */} - - - - } - /> - - - - } - /> - - - - } - /> +
+ + {/* Landing or Home */} + + + + + + + + + } + /> - {/* Public pages */} - } /> - } /> - } /> + {/* Signed-in only routes */} + } + /> + } + /> + } + /> - {/* Uncomment when ready */} - {/* - } /> - } /> - */} - -
+ {/* Public pages */} + } /> + } /> + + {/* Onboarding */} + }> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } +// Default export so main.jsx works export default function App() { return ( - - ); } diff --git a/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx b/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx new file mode 100644 index 0000000..1e059f1 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx @@ -0,0 +1,126 @@ +'use client' + +import { + forwardRef, + useEffect, + useState, + useImperativeHandle, +} from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { X } from 'lucide-react' + +// ✅ Component with forwardRef +const CreateProfile = forwardRef(({ onDataChange, formData }, ref) => { + const [firstName, setFirstName] = useState(formData?.first_name || '') + const [lastName, setLastName] = useState(formData?.last_name || '') + const [colleges, setColleges] = useState(formData?.colleges || []) + const [newCollege, setNewCollege] = useState('') + const [errors, setErrors] = useState({}) + + // Send data to parent on change + useEffect(() => { + onDataChange({ first_name: firstName, last_name: lastName, colleges }) + }, [firstName, lastName, colleges]) + + // Validation logic exposed via ref + useImperativeHandle(ref, () => ({ + validate: () => { + const newErrors = {} + if (!firstName.trim()) newErrors.firstName = 'First name is required.' + if (!lastName.trim()) newErrors.lastName = 'Last name is required.' + if (colleges.length === 0) newErrors.colleges = 'At least one college is required.' + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + }, + })) + + const handleAddCollege = () => { + const trimmed = newCollege.trim() + if (trimmed && !colleges.includes(trimmed)) { + const updated = [...colleges, trimmed].sort() + setColleges(updated) + setNewCollege('') + } + } + + const handleRemoveCollege = (college) => { + const updated = colleges.filter((c) => c !== college).sort() + setColleges(updated) + } + + return ( +
+ {/* First Name */} +
+ + setFirstName(e.target.value)} + placeholder="Enter your first name" + /> + {errors.firstName &&

{errors.firstName}

} +
+ + {/* Last Name */} +
+ + setLastName(e.target.value)} + placeholder="Enter your last name" + /> + {errors.lastName &&

{errors.lastName}

} +
+ + {/* Colleges */} +
+ +
+ setNewCollege(e.target.value)} + placeholder="Add college or university" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAddCollege() + } + }} + /> + +
+ +
+ {colleges.map((college) => ( + + {college} + + + ))} +
+ + {errors.colleges &&

{errors.colleges}

} +
+
+ ) +}) + +// ✅ Export wrapped with forwardRef +export default CreateProfile diff --git a/frontend/bitmatch/src/components/onboarding/Interest.jsx b/frontend/bitmatch/src/components/onboarding/Interest.jsx new file mode 100644 index 0000000..a1602c1 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/Interest.jsx @@ -0,0 +1,88 @@ +'use client' + +import { + forwardRef, + useEffect, + useState, + useImperativeHandle, +} from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { X } from 'lucide-react' + +const Interest = forwardRef(({ onDataChange, formData }, ref) => { + const [interests, setInterests] = useState(formData?.interests || []) + const [newInterest, setNewInterest] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + onDataChange({ interests }) + }, [interests]) + + const validate = () => { + if (interests.length === 0) { + setError('Please add at least one interest.') + return false + } + setError('') + return true + } + + useImperativeHandle(ref, () => ({ validate })) + + const handleAdd = () => { + const trimmed = newInterest.trim() + if (trimmed && !interests.includes(trimmed)) { + setInterests([...interests, trimmed].sort()) + setNewInterest('') + setError('') + } + } + + const handleRemove = (interest) => { + setInterests(interests.filter((i) => i !== interest)) + } + + return ( +
+ + +
+ setNewInterest(e.target.value)} + placeholder="Type a category" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAdd() + } + }} + /> + +
+ + {error &&

{error}

} + +
+ {interests.map((interest) => ( +
+ {interest} + +
+ ))} +
+
+ ) +}) + +export default Interest diff --git a/frontend/bitmatch/src/components/onboarding/Location.jsx b/frontend/bitmatch/src/components/onboarding/Location.jsx new file mode 100644 index 0000000..d99c234 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/Location.jsx @@ -0,0 +1,97 @@ +'use client' + +import { + forwardRef, + useEffect, + useState, + useImperativeHandle, +} from 'react' +import { Checkbox } from '@/components/ui/checkbox' + +const Location = forwardRef(({ onDataChange, formData }, ref) => { + const [location, setLocation] = useState(formData?.location || '') + const [preferences, setPreferences] = useState( + formData?.location_preferences?.filter(p => p !== '') || ["Remote friendly projects"] + ) + const [other, setOther] = useState('') + const [errors, setErrors] = useState({}) + + const options = [ + "Near my location", + "Near my University or College location", + "Remote friendly projects", + ] + + useEffect(() => { + const allPreferences = [...preferences, ...(other ? [other] : [])] + onDataChange({ location, location_preferences: allPreferences }) + }, [location, preferences, other]) + + const validate = () => { + const newErrors = {} + if (!location.trim()) newErrors.location = 'Location is required.' + if (preferences.length === 0 && !other.trim()) { + newErrors.preferences = 'Please select or enter at least one preference.' + } + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + useImperativeHandle(ref, () => ({ validate })) + + const togglePreference = (value) => { + setPreferences((prev) => + prev.includes(value) + ? prev.filter((p) => p !== value) + : [...prev, value] + ) + } + + return ( +
+
+ + setLocation(e.target.value)} + placeholder="Enter your location" + /> + {errors.location &&

{errors.location}

} +
+ +
+ + + {options.map((option) => ( +
+ togglePreference(option)} + /> + +
+ ))} + +
+ + setOther(e.target.value)} + placeholder="Add custom preference" + /> +
+ + {errors.preferences &&

{errors.preferences}

} +
+
+ ) +}) + +export default Location diff --git a/frontend/bitmatch/src/components/onboarding/Roles.jsx b/frontend/bitmatch/src/components/onboarding/Roles.jsx new file mode 100644 index 0000000..2a88cd5 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/Roles.jsx @@ -0,0 +1,88 @@ +'use client' + +import { + forwardRef, + useEffect, + useState, + useImperativeHandle, +} from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { X } from 'lucide-react' + +const Roles = forwardRef(({ onDataChange, formData }, ref) => { + const [roles, setRoles] = useState(formData?.roles || []) + const [newRole, setNewRole] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + onDataChange({ roles }) + }, [roles]) + + const validate = () => { + if (roles.length === 0) { + setError('Please add at least one role.') + return false + } + setError('') + return true + } + + useImperativeHandle(ref, () => ({ validate })) + + const handleAdd = () => { + const trimmed = newRole.trim() + if (trimmed && !roles.includes(trimmed)) { + setRoles([...roles, trimmed].sort()) + setNewRole('') + setError('') + } + } + + const handleRemove = (role) => { + setRoles(roles.filter((r) => r !== role)) + } + + return ( +
+ + +
+ setNewRole(e.target.value)} + placeholder="Type a position" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAdd() + } + }} + /> + +
+ + {error &&

{error}

} + +
+ {roles.map((role) => ( +
+ {role} + +
+ ))} +
+
+ ) +}) + +export default Roles diff --git a/frontend/bitmatch/src/components/onboarding/Skills.jsx b/frontend/bitmatch/src/components/onboarding/Skills.jsx new file mode 100644 index 0000000..cfce8cf --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/Skills.jsx @@ -0,0 +1,88 @@ +'use client' + +import { + forwardRef, + useEffect, + useState, + useImperativeHandle, +} from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { X } from 'lucide-react' + +const Skills = forwardRef(({ onDataChange, formData }, ref) => { + const [skills, setSkills] = useState(formData?.skills || []) + const [newSkill, setNewSkill] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + onDataChange({ skills }) + }, [skills]) + + const validate = () => { + if (skills.length === 0) { + setError('Please add at least one skill.') + return false + } + setError('') + return true + } + + useImperativeHandle(ref, () => ({ validate })) + + const handleAdd = () => { + const trimmed = newSkill.trim() + if (trimmed && !skills.includes(trimmed)) { + setSkills([...skills, trimmed].sort()) + setNewSkill('') + setError('') + } + } + + const handleRemove = (skill) => { + setSkills(skills.filter((s) => s !== skill)) + } + + return ( +
+ + +
+ setNewSkill(e.target.value)} + placeholder="Type a skill" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleAdd() + } + }} + /> + +
+ + {error &&

{error}

} + +
+ {skills.map((skill) => ( +
+ {skill} + +
+ ))} +
+
+ ) +}) + +export default Skills diff --git a/frontend/bitmatch/src/components/onboarding/StepIndicator.jsx b/frontend/bitmatch/src/components/onboarding/StepIndicator.jsx new file mode 100644 index 0000000..2301757 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/StepIndicator.jsx @@ -0,0 +1,20 @@ +export default function StepIndicator({ currentStep, totalSteps }) { + return ( +
+ {Array.from({ length: totalSteps }).map((_, index) => { + const isActive = index + 1 === currentStep + return ( +
+ ) + })} +
+ ) +} diff --git a/frontend/bitmatch/src/components/ui/ImageSlideshow.jsx b/frontend/bitmatch/src/components/ui/ImageSlideshow.jsx index 4b972b5..e747b76 100644 --- a/frontend/bitmatch/src/components/ui/ImageSlideshow.jsx +++ b/frontend/bitmatch/src/components/ui/ImageSlideshow.jsx @@ -67,26 +67,25 @@ export default function ImageSlideshow({ items }) { }} > {/* Darker Overlay */} -
-
-

- {slide.title} -

-

{slide.description}

- +

+ {slide.title} +

+

{slide.description}

+ +
-
+ ))} diff --git a/frontend/bitmatch/src/components/ui/badge.jsx b/frontend/bitmatch/src/components/ui/badge.jsx new file mode 100644 index 0000000..f7fe6f2 --- /dev/null +++ b/frontend/bitmatch/src/components/ui/badge.jsx @@ -0,0 +1,14 @@ +import React from "react" +import { cn } from "@/lib/utils" + +export function Badge({ className, ...props }) { + return ( +
+ ) +} diff --git a/frontend/bitmatch/src/components/ui/checkbox.jsx b/frontend/bitmatch/src/components/ui/checkbox.jsx new file mode 100644 index 0000000..b52b792 --- /dev/null +++ b/frontend/bitmatch/src/components/ui/checkbox.jsx @@ -0,0 +1,29 @@ +'use client' + +import React, { forwardRef } from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" +import { cn } from "@/lib/utils" + +const Checkbox = forwardRef((props, ref) => { + const { className, ...rest } = props + + return ( + + + + + + ) +}) + +Checkbox.displayName = "Checkbox" + +export { Checkbox } diff --git a/frontend/bitmatch/src/components/ui/toast.jsx b/frontend/bitmatch/src/components/ui/toast.jsx new file mode 100644 index 0000000..f8936f1 --- /dev/null +++ b/frontend/bitmatch/src/components/ui/toast.jsx @@ -0,0 +1,85 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva } from "class-variance-authority"; +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef(({ className, variant, ...props }, ref) => { + return ( + + ); +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction }; diff --git a/frontend/bitmatch/src/components/ui/toaster.jsx b/frontend/bitmatch/src/components/ui/toaster.jsx new file mode 100644 index 0000000..892a53d --- /dev/null +++ b/frontend/bitmatch/src/components/ui/toaster.jsx @@ -0,0 +1,33 @@ +import { useToast } from "@/hooks/use-toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/frontend/bitmatch/src/hooks/use-toast.js b/frontend/bitmatch/src/hooks/use-toast.js new file mode 100644 index 0000000..03accc0 --- /dev/null +++ b/frontend/bitmatch/src/hooks/use-toast.js @@ -0,0 +1,155 @@ +"use client"; +// Inspired by react-hot-toast library +import * as React from "react" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST" +} + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString(); +} + +const toastTimeouts = new Map() + +const addToRemoveQueue = (toastId) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state, action) => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t), + }; + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +} + +const listeners = [] + +let memoryState = { toasts: [] } + +function dispatch(action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +function toast({ + ...props +}) { + const id = genId() + + const update = (props) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + }; + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast } diff --git a/frontend/bitmatch/src/views/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index eb694c4..efb0bc0 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -56,7 +56,7 @@ export default function Home() { } return ( -
+
diff --git a/frontend/bitmatch/src/views/OnboardPage.jsx b/frontend/bitmatch/src/views/OnboardPage.jsx index ca532fd..e0c71f3 100644 --- a/frontend/bitmatch/src/views/OnboardPage.jsx +++ b/frontend/bitmatch/src/views/OnboardPage.jsx @@ -1,82 +1,182 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/Label"; -import { Textarea } from "@/components/ui/Textarea"; +'use client' + +import { useState, useRef } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { motion, AnimatePresence } from 'framer-motion' +import { Button } from '@/components/ui/button' +import { Toaster } from '@/components/ui/toaster' +import CreateProfile from '@/components/onboarding/CreateProfile' +import Location from '@/components/onboarding/Location' +import Roles from '@/components/onboarding/Roles' +import Interest from '@/components/onboarding/Interest' +import Skills from '@/components/onboarding/Skills' +import StepIndicator from '@/components/onboarding/StepIndicator' export default function OnboardPage() { - const navigate = useNavigate(); - const [formData, setFormData] = useState({ - role: "", - interests: "", - bio: "", - }); + const [currentStep, setCurrentStep] = useState(1) + const [formData, setFormData] = useState({}) + const totalSteps = 5 + + const createProfileRef = useRef() + const locationRef = useRef() + const rolesRef = useRef() + const interestRef = useRef() + const skillsRef = useRef() + + const stepTitles = [ + 'Create your Profile', + 'Location', + 'Roles & Positions', + 'Project Interests', + 'List Skill Sets', + ] + + const updateStepData = (data) => { + setFormData((prev) => ({ ...prev, ...data })) + } + + const validateStep = () => { + switch (currentStep) { + case 1: + return createProfileRef.current?.validate() ?? false + case 2: + return locationRef.current?.validate() ?? false + case 3: + return rolesRef.current?.validate() ?? false + case 4: + return interestRef.current?.validate() ?? false + case 5: + return skillsRef.current?.validate() ?? false + default: + return true + } + } - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); - }; + const handleNext = () => { + if (validateStep()) { + setCurrentStep(currentStep + 1) + } else { + toast({ + variant: 'destructive', + title: '🚫 Incomplete Step', + description: 'Please complete this step before proceeding.', + }) + } + } - const handleSubmit = (e) => { - e.preventDefault(); + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1) + } + } - // You can send this data to Supabase, Clerk user metadata, or your backend here - console.log("Onboarding data submitted:", formData); + const handleSubmit = async () => { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/user-profiles/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) - // Redirect to dashboard or home - navigate("/dashboard"); - }; + if (!res.ok) throw new Error('Failed to submit') + alert('✅ Profile submitted successfully!') + } catch (err) { + console.error(err) + alert('❌ Submission failed') + } + } + + const renderStep = () => { + const stepProps = { onDataChange: updateStepData, formData } + + switch (currentStep) { + case 1: + return + case 2: + return + case 3: + return + case 4: + return + case 5: + return + default: + return null + } + } return ( -
-
-

Work in Progress!

-

- This is just a placeholder for the onboarding process. Nothing works - right now. -

- -
-
- - -
- -
- - -
- -
- - diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index a9cd280..c48a444 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -12,21 +12,14 @@ import { Input } from "@/components/ui/input"; import { MemberCard } from "@/components/project/MemberCard"; import { PositionCard } from "@/components/project/PositionCard"; import React, { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { DiscussionPost, ReplyForm } from "@/components/project/DiscussionCard"; import { EditProjectDialog } from "@/components/project/EditProjectDialog"; +import { useUser } from "@clerk/clerk-react"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; import axios from "axios"; -const formatNumber = (num) => { - return num >= 1000000 - ? `${(num / 1000000).toFixed(1)}M`.replace(".0M", "M") - : num >= 1000 - ? `${(num / 1000).toFixed(1)}K`.replace(".0K", "K") - : num.toString(); -}; - const fetchProjectInfo = async (id) => { try { const response = await fetch(`${SERVER_HOST}/projects/${id}/`); @@ -47,6 +40,8 @@ const fetchProjectInfo = async (id) => { const editProjectInfo = async (id) => {}; const ProjectDetailPage = () => { + const navigate = useNavigate(); + const { user } = useUser(); const { id } = useParams(); // Access the dynamic `id` parameter from the URL const [project, setProject] = useState(null); // State to store project details const [loading, setLoading] = useState(true); // State to handle loading state @@ -57,8 +52,26 @@ const ProjectDetailPage = () => { const [discussions, setDiscussions] = useState([]); const [showCommentForm, setShowCommentForm] = useState(false); const [replyingTo, setReplyingTo] = useState(null); - const [following, setFollowing ] = useState(false); - const [likeStatus, setLiked] = useState(false); + const [following, setFollowing] = useState(false); + const [likeStatus, setLiked] = useState(false); + const [userUuid, setUserUuid] = useState(null); + + useEffect(() => { + const fetchUserUuid = async () => { + try { + const response = await fetch(`${SERVER_HOST}/userauth/${user.id}`); + if (!response.ok) { + throw new Error("Failed to fetch user data"); + } + const data = await response.json(); + setUserUuid(data.id); + } catch (error) { + console.error("Error fetching user UUID:", error); + } + }; + + fetchUserUuid(); + }, [user.id]); useEffect(() => { // Fetch project details when the component mounts or the `id` changes @@ -83,41 +96,43 @@ const ProjectDetailPage = () => { const handleFollow = async () => { setFollowing(!following); - console.log("current following status:", following) + console.log("current following status:", following); const fdata = { - "action": following ? "unfollow" : "follow", - "user_id" : 1 + action: following ? "unfollow" : "follow", + user_id: 1, }; - console.log("id is ", id) - + console.log("id is ", id); + try { const response = await axios.post( - `${SERVER_HOST}/projects/follow/${id}`, fdata, - ) + `${SERVER_HOST}/projects/follow/${id}`, + fdata + ); console.log("response: ", response.data); window.location.reload(); } catch (error) { - console.error('Error updating follows: ', error); + console.error("Error updating follows: ", error); } }; const handleLike = async () => { setLiked(!likeStatus); - console.log("current like status:", likeStatus) + console.log("current like status:", likeStatus); const fdata = { - "action": likeStatus ? "unlike" : "like", - "user_id" : 1 + action: likeStatus ? "unlike" : "like", + user_id: 1, }; - console.log("id is ", id) - + console.log("id is ", id); + try { const response = await axios.post( - `${SERVER_HOST}/projects/like/${id}`, fdata, - ) + `${SERVER_HOST}/projects/like/${id}`, + fdata + ); console.log("response: ", response.data); window.location.reload(); } catch (error) { - console.error('Error updating likes: ', error); + console.error("Error updating likes: ", error); } }; @@ -303,7 +318,7 @@ const ProjectDetailPage = () => {
- ); - } - - // Error state - if (error) { - return ( -
-

{error}

-
- ); - } - - if (!project) { - return
No project found.
; + useEffect(() => { + const fetchProfileData = async () => { + try { + const response = await fetch(`${SERVER_HOST}/userauth/${id}`); + if (!response.ok) { + throw new Error("Failed to fetch profile data"); + } + const data = await response.json(); + setProfile(data); + setUserUuid(data.id); + fetchProjectDetails(data.projects); + } catch (error) { + console.error("Error fetching profile data:", error); + } finally { + setLoading(false); } - */ + }; + + const fetchProjectDetails = async (projectIds) => { + const projects = await Promise.all( + projectIds.map(async (projectId) => { + try { + const response = await fetch( + `${SERVER_HOST}/projects/${projectId}/` + ); + const project = await response.json(); + return project; + } catch (error) { + console.error("Error fetching project:", error); + return null; + } + }) + ); + setProjectsData(projects.filter((project) => project !== null)); + }; + + fetchProfileData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleLeftArrowClick = () => { + setCurrentIndex((prevIndex) => + prevIndex === 0 ? projectsData.length - 1 : prevIndex - 1 + ); + }; + + const handleRightArrowClick = () => { + setCurrentIndex((prevIndex) => + prevIndex === projectsData.length - 1 ? 0 : prevIndex + 1 + ); + }; + + if (loading) { + return ( +
+
+
+ ); + } + if (!profile) { return ( -
- {/* Main Content */} -
- {/* Profile Section */} -
- {/* Background Image */} -
Background Image
- -
- {/* Profile Image */} -
- Img Profile -
- -
- - -
- -
-

Student's Name

-

Position Title Preference

-

Name of University or College

-

Student's Major

- -
-
-
- 2,000,000 Followers -
- -
-
+
+

No profile found.

+
+ ); + } + + return ( +
+
+
+
+ +
+
+ {user?.imageUrl && user.id == id ? ( + Profile + ) : ( +
+ )} +
+ + {user?.id === id && ( +
+ + +
+ )} +
+

+ {profile.first_name} {profile.last_name} +

+

+ Student @ {profile.college} +

+

{profile.location}

+ +

+ Email:{" "} + + {profile.email} + +

+
+
+
+ +
+

About

+

{profile.about_me}

+
+ +
+

Projects

+ +
+
+ {projectsData + .slice(currentIndex, currentIndex + 3) + .map((project, index) => ( + +
+ {project.image_url && ( + {project.title} + )}
-
- - {/* About Section */} -
-

About

-

- Summary of the Student. A small description of the student can be typed here. It won't be very long. - Standard dummy text. A small description of the Student can be typed here. It won't be very long. Standard - dummy text, standard dummy text. Summary of the Student. A small description of the student can be typed - here. It won't be very long. Standard standard dummy text. A small description of the Student can be typed - here. It won't be very long. Standard dummy text, text, standard dummy text. Summary of the Student. A small - description of the student can be typed here. It won't be very long. Standard dummy text, standard dummy - text. +

+ {project.title} + + {project.owner === userUuid ? "– Owner" : "– Member"} +

-
- - {/* Current Projects */} -
-

Current Projects

- -
-
- {[1, 2, 3].map((project) => ( -
-
-

The Name of the Project will go here. The Name of the Project will go here

-
- ))} -
- - - -
-
- - {/* Education */} -
-

Education

- - -
-
-
- -
-
-

California State Polytechnic University, Pomona - May 2026

-

BA, Computer Science

-
-
- -
-
- -
-
-

Citrus College, Glendora - May 2023

-

AS-T, Computer Science

-
-
-
-
- - {/* Positions that Interest Me */} -
-

Positions that Interest Me

- - -
- - Front-End Development - - - Full-stack Development - - - Backend Development - - - Data Engineer - -
-
- - {/* Skills */} -
-

Skills

- - -
- - Python - - - HTML - - - CSS - - - Analytical - - - Communication Skills - -
-
-
+ + ))} +
+ + + + +
+
+ +
+

Education

+ +
+
+
+ +
+
+

+ {profile.college} - {profile.grad_date} +

+

{profile.major}

+
+
+
+
+ +
+

Desired Positions

+ +
+ {profile.roles.map((role, index) => ( + + {role} + + ))} +
+
+ +
+

Interests

+ +
+ {profile.interests.map((interest, index) => ( + + {interest} + + ))} +
+
+ +
+

Skills

+ +
+ {profile.skills.map((skill, index) => ( + + {skill} + + ))} +
- ) +
+
+ ); } From 93a455886bb7de9c8b85742ba893b42da8868dba Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:25:40 -0700 Subject: [PATCH 08/17] chore: pull data and pass into matchscoreutils --- frontend/bitmatch/src/views/MatchScore.js | 82 ----------- .../bitmatch/src/views/MatchScoreUtils.js | 137 ++++++++++++++++++ .../bitmatch/src/views/ProjectListPage.jsx | 46 ++++-- 3 files changed, 170 insertions(+), 95 deletions(-) delete mode 100644 frontend/bitmatch/src/views/MatchScore.js create mode 100644 frontend/bitmatch/src/views/MatchScoreUtils.js diff --git a/frontend/bitmatch/src/views/MatchScore.js b/frontend/bitmatch/src/views/MatchScore.js deleted file mode 100644 index e01a2b8..0000000 --- a/frontend/bitmatch/src/views/MatchScore.js +++ /dev/null @@ -1,82 +0,0 @@ -let projects = fetch('http://localhost:5432/', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => response.json()) - .then(data => console.log(data)) - .catch(error => console.error('Error fetching projects:', error)); - -let user = fetch('http://localhost:8000/user_123/') - .then(res => res.json()) - .then(data => console.log('User data:', data)) - .catch(error => console.error('Error fetching user:', error)); - -function calculateMatchScore(Project, User) { - // Define weights for each attribute (optional) - const weights = { - school: 0.25, - jobTitle: 0.15, - tags: 0.60 - }; - - // Function to compare two values and return a score (0 to 1) - function compareValues(valueA, valueB) { - if (valueA === valueB) { - return 1; // Perfect match - } else { - // For non-numeric values, return 0 if they don't match - return 0; - } - } - - // Function to compare tags of Project and User - function compareTags(tagsP, tagsU){ - let count = 0; // Value to keep Track of how many instances of - let score = 0.0; - for(let i = 0; i < tagsP.length; i++){ - for(let j = 0; j < tagsP.length; j ++){ - if (tagsU[i] == tagsP[j]){ - count ++; - } - } - } - score = (count/tagsP.length); - return score; - } - // Calculate scores for each attribute - const score1 = compareValues(Project.school, User.school) - const score2 = compareValues(Project.jobTitle, User.jobTitle) - const score3 = compareTags(Project.tags, User.tags) - // Sum the scores - const totalScore = (score1 * 0.25) + (score2 * 0.15) + (score3 * 0.6); - - // Normalize the score to a range of 0 to 100 - const normalizedScore = Math.round(totalScore * 100); - return normalizedScore; -} - -function main(){ - let scores = []; - for(let i = projects; projects.length(); i++){ - scores.push(calculateMatchScore(projects[i], user)); - } - scores.sort((a,b) => a-b); -} -// Example usage -/*const Project = { - school: "Cal Poly Pomona", - jobTitle: "Data Scientist", - tags: ["Python", "Data Science"] -}; - -const User = { - school: "Cal Poly Pomona", - jobTitle: "Software Engineer", - tags: ["Python", "Object Orientated Programing"] -}; - - -let sample = calculateMatchScore(Project, User); -console.log(sample);*/ \ No newline at end of file diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js new file mode 100644 index 0000000..7489492 --- /dev/null +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -0,0 +1,137 @@ +// function calculateMatchScore(Project, User) { +// // Define weights for each attribute (optional) +// const weights = { +// school: 0.25, +// jobTitle: 0.15, +// tags: 0.6, +// }; + +// // Function to compare two values and return a score (0 to 1) +// function compareValues(valueA, valueB) { +// if (valueA === valueB) { +// return 1; // Perfect match +// } else { +// // For non-numeric values, return 0 if they don't match +// return 0; +// } +// } + +// // Function to compare tags of Project and User +// function compareTags(tagsP, tagsU) { +// let count = 0; // Value to keep Track of how many instances of +// let score = 0.0; +// for (let i = 0; i < tagsP.length; i++) { +// for (let j = 0; j < tagsP.length; j++) { +// if (tagsU[i] == tagsP[j]) { +// count++; +// } +// } +// } +// score = count / tagsP.length; +// return score; +// } +// // Calculate scores for each attribute +// const score1 = compareValues(Project.school, User.school); +// const score2 = compareValues(Project.jobTitle, User.jobTitle); +// const score3 = compareTags(Project.tags, User.tags); +// // Sum the scores +// const totalScore = score1 * 0.25 + score2 * 0.15 + score3 * 0.6; + +// // Normalize the score to a range of 0 to 100 +// const normalizedScore = Math.round(totalScore * 100); +// return normalizedScore; +// } + +// function main() { +// let scores = []; +// for (let i = projects; projects.length(); i++) { +// scores.push(calculateMatchScore(projects[i], user)); +// } +// scores.sort((a, b) => a - b); +// } +// // Example usage +// /*const Project = { +// school: "Cal Poly Pomona", +// jobTitle: "Data Scientist", +// tags: ["Python", "Data Science"] +// }; + +// const User = { +// school: "Cal Poly Pomona", +// jobTitle: "Software Engineer", +// tags: ["Python", "Object Orientated Programing"] +// }; + +// let sample = calculateMatchScore(Project, User); +// console.log(sample);*/ + +// Placeholder function to calculate the match score +// TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. +// PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. +export const calculateMatchScores = (userData, projects) => { + console.log(userData); + + // Mock User Profile: + // { + // id: "610c6c0f-dcf2-432f-b64b-9ec94481ef8d", + // auth_id: "user_2tTc9slPZTzbFvJWsujHu4MMg62", + // first_name: "Larry", + // last_name: "La", + // username: "larrylaa", + // email: "larryquocla@gmail.com", + // about_me: "As a rising junior computer science student at California State Polytechnic University-Pomona, I am passionate about learning and applying new technologies to create impactful software.", + // college: "Cal Poly Pomona", + // major: "Computer Science", + // grad_date: "May 2027", + // location: "El Monte, CA", + // interests: ["Backend", "Cloud Computing", "DevOps", "Frontend"], + // roles: ["Backend Web Developer", "Cloud Engineer", "Cloud Security Engineer", "DevOps Engineer", "Software Engineer"], + // skills: ["AWS", "Azure", "Back-End Web Development", "C#", "ExpressJS", "Git/Github", "HTML/CSS", "Java", "JavaScript", "Postgres", "Powershell", "Python", "React", "SQL", "Terraform", "VBA"], + // location_preferences: ["Near my University or College location", "Remote friendly projects", "Near my location"], + // followers: 0, + // likes: 0, + // owned: [], + // projects: ["67e6f820-b053-48d8-a7dd-db11396d3310", "37bfe931-7be0-42b1-ab85-abd110c22ff7"] + // } + + console.log(projects); + + // Mock Project: + // { + // id: "4cca4a32-ba4b-4ad7-ab4b-e298d428ef5a", + // title: "Mobile Pirate Game", + // description: "This is a project to make a mobile side scrolling pirate game.", + // full_description: "This is a project to make a mobile side scrolling pirate game.", + // image_url: "https://bitmatch-django-media-bucket.s3.amazonaws.com/projects/home_bg_landscape.png", + // institution: "California Polytechnic State University Pomona", + // owner: "db164a04-b296-41f8-b70d-ef31cda89f3e", + // followers_count: 0, + // likes_count: 0, + // positions: [ + // // Example structure: + // // { + // // title: "Frontend Developer", + // // skills_required: ["React", "JavaScript"], + // // filled: false + // // } + // ], + // location: [], + // interest_tags: null, + // skill_tags: null, + // group: "", + // email: null, + // other_contact: null, + // updates: null, + // wanted_description: null, + // } + + return projects.map((project) => { + // Placeholder logic: randomly assign a score between 1 and 100 + const match_percentage = Math.floor(Math.random() * 100) + 1; + + return { + ...project, + match_percentage: match_percentage, + }; + }); +}; diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index ff62909..06384a2 100644 --- a/frontend/bitmatch/src/views/ProjectListPage.jsx +++ b/frontend/bitmatch/src/views/ProjectListPage.jsx @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { ChevronRight, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Link } from "react-router-dom"; import ProjectCardLarge from "@/components/project/ProjectCardLarge"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; import axios from "axios"; import { useNavigate } from "react-router-dom"; import { useUser } from "@clerk/clerk-react"; +import { calculateMatchScores } from "./MatchScoreUtils"; export default function ProjectListPage() { const navigate = useNavigate(); @@ -46,22 +46,42 @@ export default function ProjectListPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + useEffect(() => { - axios - .get(`${SERVER_HOST}/projects/`) - .then((response) => { - const sortedProjects = response.data.sort( + const fetchUserAndProjects = async () => { + try { + // Fetch user data + const userResponse = await fetch(`${SERVER_HOST}/userauth/${user.id}`); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = await userResponse.json(); + + // Fetch project data + const projectsResponse = await axios.get(`${SERVER_HOST}/projects/`); + const projects = projectsResponse.data; + + // Pass both userData and projects to a function for match score calculation + const projectsWithScores = calculateMatchScores(userData, projects); + + // Sort projects based on match score + const sortedProjects = projectsWithScores.sort( (a, b) => b.match_percentage - a.match_percentage ); + + // Update state with sorted projects setProjects(sortedProjects); + console.log(sortedProjects); + } catch (error) { + console.error("Error fetching user and projects:", error); + setError("Couldn't load projects or user data."); + } finally { setLoading(false); - }) - .catch((error) => { - setError("Couldn't load projects."); - console.error("Error fetching projects:", error); - setLoading(false); - }); - }, []); + } + }; + + fetchUserAndProjects(); + }, [user.id]); if (loading) { return ( From 149a767a44917f2daa0f5e94e373092b3d63a8b4 Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:30:08 -0700 Subject: [PATCH 09/17] chore: add for home page also --- frontend/bitmatch/src/views/HomePage.jsx | 44 +++++++++++++++++------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/bitmatch/src/views/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index ffc3673..678bc9d 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -4,6 +4,7 @@ import ImageSlideshow from "../components/ui/ImageSlideshow"; import ProjectCarousel from "../components/ui/ProjectCarousel"; import { useNavigate } from "react-router-dom"; import { useUser } from "@clerk/clerk-react"; +import { calculateMatchScores } from "./MatchScoreUtils"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; @@ -55,23 +56,40 @@ export default function Home() { // Fetch project data on component mount useEffect(() => { - axios - .get(`${SERVER_HOST}/projects/`) - .then((response) => { - const sortedProjects = response.data.sort( + const fetchUserAndProjects = async () => { + try { + // Fetch user data + const userResponse = await fetch(`${SERVER_HOST}/userauth/${user.id}`); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = await userResponse.json(); + + // Fetch project data + const projectsResponse = await axios.get(`${SERVER_HOST}/projects/`); + const projects = projectsResponse.data; + + // Pass both userData and projects to a function for match score calculation + const projectsWithScores = calculateMatchScores(userData, projects); + + // Sort projects based on match score + const sortedProjects = projectsWithScores.sort( (a, b) => b.match_percentage - a.match_percentage ); + + // Update state with sorted projects setProjects(sortedProjects); + console.log(sortedProjects); + } catch (error) { + console.error("Error fetching user and projects:", error); + setError("Couldn't load projects or user data."); + } finally { setLoading(false); - }) - .catch((error) => { - console.error("Error fetching projects:", error); - setError( - "Couldn't load projects. (DEV MESSAGE: Ensure the backend server is running)" - ); - setLoading(false); - }); - }, []); + } + }; + + fetchUserAndProjects(); + }, [user.id]); // Loading state if (loading) { From c431335baa129823698250f0a6dc75974e56d3b2 Mon Sep 17 00:00:00 2001 From: Larry La Date: Fri, 18 Apr 2025 12:35:31 -0700 Subject: [PATCH 10/17] chore: abstract user error msg --- frontend/bitmatch/src/views/HomePage.jsx | 2 +- frontend/bitmatch/src/views/ProjectListPage.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/bitmatch/src/views/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index 678bc9d..3cd1f4c 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -82,7 +82,7 @@ export default function Home() { console.log(sortedProjects); } catch (error) { console.error("Error fetching user and projects:", error); - setError("Couldn't load projects or user data."); + setError("Something went wrong. Please try again later."); } finally { setLoading(false); } diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index 06384a2..1f320e7 100644 --- a/frontend/bitmatch/src/views/ProjectListPage.jsx +++ b/frontend/bitmatch/src/views/ProjectListPage.jsx @@ -74,7 +74,7 @@ export default function ProjectListPage() { console.log(sortedProjects); } catch (error) { console.error("Error fetching user and projects:", error); - setError("Couldn't load projects or user data."); + setError("Something went wrong. Please try again later."); } finally { setLoading(false); } From c882e2d168f2acf88edd426fc76bc4c1b232630a Mon Sep 17 00:00:00 2001 From: ColumnSkunky Date: Sat, 19 Apr 2025 19:51:26 -0700 Subject: [PATCH 11/17] fix: MatchScoreUtils.js to calculate new user values as well as correctly outputs weighted scores --- .../bitmatch/src/views/MatchScoreUtils.js | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index 7489492..247bb14 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -6,6 +6,8 @@ // tags: 0.6, // }; +import ProjectCardLarge from "@/components/project/ProjectCardLarge"; + // // Function to compare two values and return a score (0 to 1) // function compareValues(valueA, valueB) { // if (valueA === valueB) { @@ -64,12 +66,52 @@ // let sample = calculateMatchScore(Project, User); // console.log(sample);*/ +export function compareTags (userTags, projectTags) { + /*const sortedUserTags = userTags.sort(); + const sortedProjectTags = projectTags.sort(); + const count = 0; + for(let i = 0; i < sortedProjectTags.length; i ++){ + if (sortedUserTags[i] == sortedProjectTags[i]){ + count ++ + } + }*/ + + const userSet = new Set(userTags); + const projectSet = new Set(projectTags); + let matches = 0; + for(let tags of projectSet){ + if(userSet.has(tags)){ + matches ++; + } + } + + return matches / projectSet.size; +} + +export function compareValues (userAtt, projectAtt){ + if(userAtt == projectAtt){ + return 1; + } + else{ + return 0; + } +} +export function roleSearch(userRole, projectPosition){ + const userSet = new Set(userRole); + const projectSet = new Set(projectPosition); + for (let role of projectSet){ + if (userSet.has(role)){ + return 1; + } + } + return 0; +} // Placeholder function to calculate the match score // TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. // PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. export const calculateMatchScores = (userData, projects) => { - console.log(userData); + console.log(userData.location); // Mock User Profile: // { @@ -124,11 +166,29 @@ export const calculateMatchScores = (userData, projects) => { // updates: null, // wanted_description: null, // } - + + //3 institution 15 + //5 location 5 + //4 intrest tags 10 + //1 skill tags 50 + //2 positions/roles 20 + + return projects.map((project) => { - // Placeholder logic: randomly assign a score between 1 and 100 - const match_percentage = Math.floor(Math.random() * 100) + 1; + + + const interests = compareTags(userData.interests, project.interest_tags || []); + const skills = compareTags(userData.skills, project.skill_tags || []); + const school = compareValues(userData.college, project.institution); + const location = compareValues(userData.location, project.location?.[0] || ''); + const role = roleSearch(userData.roles, project.positions); + + const weightedScore = (interests * 0.1) + (skills * 0.5) + (school * 0.15) + (location * 0.05) + (role * 0.2); + + //Creates a match percentage and rounds the value + const match_percentage = Math.round(weightedScore * 100); + return { ...project, match_percentage: match_percentage, From edae24ef28a35e59a96db64815cf1c24fdf53926 Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 20:13:08 -0700 Subject: [PATCH 12/17] feat: new calculation logic --- .../bitmatch/src/views/MatchScoreUtils.js | 230 ++++-------------- 1 file changed, 49 insertions(+), 181 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index 247bb14..d108bde 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -1,197 +1,65 @@ -// function calculateMatchScore(Project, User) { -// // Define weights for each attribute (optional) -// const weights = { -// school: 0.25, -// jobTitle: 0.15, -// tags: 0.6, -// }; - -import ProjectCardLarge from "@/components/project/ProjectCardLarge"; - -// // Function to compare two values and return a score (0 to 1) -// function compareValues(valueA, valueB) { -// if (valueA === valueB) { -// return 1; // Perfect match -// } else { -// // For non-numeric values, return 0 if they don't match -// return 0; -// } -// } - -// // Function to compare tags of Project and User -// function compareTags(tagsP, tagsU) { -// let count = 0; // Value to keep Track of how many instances of -// let score = 0.0; -// for (let i = 0; i < tagsP.length; i++) { -// for (let j = 0; j < tagsP.length; j++) { -// if (tagsU[i] == tagsP[j]) { -// count++; -// } -// } -// } -// score = count / tagsP.length; -// return score; -// } -// // Calculate scores for each attribute -// const score1 = compareValues(Project.school, User.school); -// const score2 = compareValues(Project.jobTitle, User.jobTitle); -// const score3 = compareTags(Project.tags, User.tags); -// // Sum the scores -// const totalScore = score1 * 0.25 + score2 * 0.15 + score3 * 0.6; - -// // Normalize the score to a range of 0 to 100 -// const normalizedScore = Math.round(totalScore * 100); -// return normalizedScore; -// } - -// function main() { -// let scores = []; -// for (let i = projects; projects.length(); i++) { -// scores.push(calculateMatchScore(projects[i], user)); -// } -// scores.sort((a, b) => a - b); -// } -// // Example usage -// /*const Project = { -// school: "Cal Poly Pomona", -// jobTitle: "Data Scientist", -// tags: ["Python", "Data Science"] -// }; +// Helper: Compare simple values (school, jobTitle) +function compareValues(valueA, valueB) { + return valueA === valueB ? 1 : 0; +} -// const User = { -// school: "Cal Poly Pomona", -// jobTitle: "Software Engineer", -// tags: ["Python", "Object Orientated Programing"] -// }; +// Helper: Compare tags (intersection similarity) +function compareTags(tagsA, tagsB) { + if (!tagsA || !tagsB || !Array.isArray(tagsA) || !Array.isArray(tagsB)) + return 0; -// let sample = calculateMatchScore(Project, User); -// console.log(sample);*/ -export function compareTags (userTags, projectTags) { - /*const sortedUserTags = userTags.sort(); - const sortedProjectTags = projectTags.sort(); - const count = 0; - for(let i = 0; i < sortedProjectTags.length; i ++){ - if (sortedUserTags[i] == sortedProjectTags[i]){ - count ++ - } - }*/ + const setA = new Set(tagsA.map((tag) => tag.toLowerCase())); + const setB = new Set(tagsB.map((tag) => tag.toLowerCase())); - const userSet = new Set(userTags); - const projectSet = new Set(projectTags); - let matches = 0; - for(let tags of projectSet){ - if(userSet.has(tags)){ - matches ++; - } - } - - return matches / projectSet.size; + const intersection = [...setA].filter((tag) => setB.has(tag)); + return intersection.length / Math.max(setA.size, 1); // Avoid division by zero } -export function compareValues (userAtt, projectAtt){ - if(userAtt == projectAtt){ - return 1; - } - else{ - return 0; +// Main: Calculate match score for one project and one user +function calculateMatchScore(project, user) { + // Weights + const weights = { + school: 0.25, + jobTitle: 0.15, + tags: 0.6, + }; + + const schoolScore = compareValues( + project.institution?.toLowerCase(), + user.college?.toLowerCase() + ); + + // Compare against ALL project positions and get best matching role/title + let jobScore = 0; + if (project.positions?.length) { + jobScore = Math.max( + ...project.positions.map((pos) => + user.roles.includes(pos.title) ? 1 : 0 + ) + ); } -} -export function roleSearch(userRole, projectPosition){ - const userSet = new Set(userRole); - const projectSet = new Set(projectPosition); - for (let role of projectSet){ - if (userSet.has(role)){ - return 1; - } - } - return 0; -} -// Placeholder function to calculate the match score -// TODO: MAKE THIS ACTUALLY CALCULATE CORRECTLY BASED OFF DATA, RN IT IS RANDOMLY CALCULATING BELOW ARE THE EXAMPLES OF THE DATA PASSED IN. -// PLEASE USE HELPER FUNCTIONS, ONE GIANT FUNCTION NOT IDEAL. -export const calculateMatchScores = (userData, projects) => { - console.log(userData.location); + // Compare tags: interest_tags and skill_tags + const interestScore = compareTags(project.interest_tags, user.interests); + const skillScore = compareTags(project.skill_tags, user.skills); + const tagScore = (interestScore + skillScore) / 2; - // Mock User Profile: - // { - // id: "610c6c0f-dcf2-432f-b64b-9ec94481ef8d", - // auth_id: "user_2tTc9slPZTzbFvJWsujHu4MMg62", - // first_name: "Larry", - // last_name: "La", - // username: "larrylaa", - // email: "larryquocla@gmail.com", - // about_me: "As a rising junior computer science student at California State Polytechnic University-Pomona, I am passionate about learning and applying new technologies to create impactful software.", - // college: "Cal Poly Pomona", - // major: "Computer Science", - // grad_date: "May 2027", - // location: "El Monte, CA", - // interests: ["Backend", "Cloud Computing", "DevOps", "Frontend"], - // roles: ["Backend Web Developer", "Cloud Engineer", "Cloud Security Engineer", "DevOps Engineer", "Software Engineer"], - // skills: ["AWS", "Azure", "Back-End Web Development", "C#", "ExpressJS", "Git/Github", "HTML/CSS", "Java", "JavaScript", "Postgres", "Powershell", "Python", "React", "SQL", "Terraform", "VBA"], - // location_preferences: ["Near my University or College location", "Remote friendly projects", "Near my location"], - // followers: 0, - // likes: 0, - // owned: [], - // projects: ["67e6f820-b053-48d8-a7dd-db11396d3310", "37bfe931-7be0-42b1-ab85-abd110c22ff7"] - // } + // Total weighted score + const totalScore = + schoolScore * weights.school + + jobScore * weights.jobTitle + + tagScore * weights.tags; - console.log(projects); + return Math.round(totalScore * 100); // Normalize to 0–100 +} - // Mock Project: - // { - // id: "4cca4a32-ba4b-4ad7-ab4b-e298d428ef5a", - // title: "Mobile Pirate Game", - // description: "This is a project to make a mobile side scrolling pirate game.", - // full_description: "This is a project to make a mobile side scrolling pirate game.", - // image_url: "https://bitmatch-django-media-bucket.s3.amazonaws.com/projects/home_bg_landscape.png", - // institution: "California Polytechnic State University Pomona", - // owner: "db164a04-b296-41f8-b70d-ef31cda89f3e", - // followers_count: 0, - // likes_count: 0, - // positions: [ - // // Example structure: - // // { - // // title: "Frontend Developer", - // // skills_required: ["React", "JavaScript"], - // // filled: false - // // } - // ], - // location: [], - // interest_tags: null, - // skill_tags: null, - // group: "", - // email: null, - // other_contact: null, - // updates: null, - // wanted_description: null, - // } - - //3 institution 15 - //5 location 5 - //4 intrest tags 10 - //1 skill tags 50 - //2 positions/roles 20 - - +// Entry Point: Calculate for all projects +export const calculateMatchScores = (userData, projects) => { return projects.map((project) => { - - - const interests = compareTags(userData.interests, project.interest_tags || []); - const skills = compareTags(userData.skills, project.skill_tags || []); - const school = compareValues(userData.college, project.institution); - const location = compareValues(userData.location, project.location?.[0] || ''); - const role = roleSearch(userData.roles, project.positions); - - const weightedScore = (interests * 0.1) + (skills * 0.5) + (school * 0.15) + (location * 0.05) + (role * 0.2); - - - //Creates a match percentage and rounds the value - const match_percentage = Math.round(weightedScore * 100); - + const match_percentage = calculateMatchScore(project, userData); return { ...project, - match_percentage: match_percentage, + match_percentage, }; }); }; From 009aff4ceb3cfe07f3aa093884560affabbcd87a Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 20:59:54 -0700 Subject: [PATCH 13/17] chore: make tags be added on project creation --- .../bitmatch/src/views/AddProjectPage.jsx | 138 +++++++++++++++++- .../src/views/IndividualProjectPage.jsx | 33 ++++- 2 files changed, 168 insertions(+), 3 deletions(-) diff --git a/frontend/bitmatch/src/views/AddProjectPage.jsx b/frontend/bitmatch/src/views/AddProjectPage.jsx index 7250248..995ad9e 100644 --- a/frontend/bitmatch/src/views/AddProjectPage.jsx +++ b/frontend/bitmatch/src/views/AddProjectPage.jsx @@ -12,6 +12,12 @@ const CREATE_PROJECT_ENDPOINT = `${SERVER_HOST}/projects/create/`; export default function CreateProjectForm() { const navigate = useNavigate(); const { user } = useUser(); + const [interests, setInterests] = useState([]); + const [newInterest, setNewInterest] = useState(""); + const [interestError, setInterestError] = useState(""); + const [skills, setSkills] = useState([]); + const [newSkill, setNewSkill] = useState(""); + const [skillError, setSkillError] = useState(""); const [projectName, setProjectName] = useState(""); const [university, setUniversity] = useState(""); const [group, setGroup] = useState(""); @@ -139,6 +145,13 @@ export default function CreateProjectForm() { formData.append("positions", JSON.stringify(roles)); formData.append("image_url", coverImageFile); formData.append("owner", userUuid); + skills.forEach((skill) => { + formData.append("skill_tags", skill); + }); + + interests.forEach((interest) => { + formData.append("interest_tags", interest); + }); try { const response = await fetch(CREATE_PROJECT_ENDPOINT, { @@ -185,8 +198,6 @@ export default function CreateProjectForm() { } finally { setIsLoading(false); } - - // TODO: api call to add project to user also. }; return ( @@ -406,6 +417,129 @@ export default function CreateProjectForm() { )}
+
+ +
+ { + setNewInterest(e.target.value); + setInterestError(""); + }} + className="flex-grow border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., Backend, Frontend, DevOps, AI" + /> + +
+ {interestError && ( +

{interestError}

+ )} +
+ {interests.length === 0 ? ( +

+ No interests added yet. +

+ ) : ( + interests.map((interest, index) => ( +
+ {interest} + +
+ )) + )} +
+
+ +
+ +
+ { + setNewSkill(e.target.value); + setSkillError(""); + }} + className="flex-grow border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., JavaScript, Figma, Django" + /> + +
+ {skillError && ( +

{skillError}

+ )} +
+ {skills.length === 0 ? ( +

+ No skills added yet. +

+ ) : ( + skills.map((skill, index) => ( +
+ {skill} + +
+ )) + )} +
+
{error && (

diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index c48a444..2cd000d 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -10,6 +10,7 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MemberCard } from "@/components/project/MemberCard"; +import { Badge } from "@/components/ui/badge"; import { PositionCard } from "@/components/project/PositionCard"; import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; @@ -535,11 +536,41 @@ const ProjectDetailPage = () => {

Overview

-

+

Background & More Details About the Project

{project.full_description}

+ +
+

Project Categories

+ +
+ {project.interest_tags.map((interest, index) => ( + + {interest} + + ))} +
+ +

Desired Skills

+ +
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+
From f37f8823c78d97c607722fe5884368c368c2992b Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 21:38:38 -0700 Subject: [PATCH 14/17] chore: make score sseem more natural --- .../bitmatch/src/views/MatchScoreUtils.js | 80 ++++++++++++------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js index d108bde..1c66514 100644 --- a/frontend/bitmatch/src/views/MatchScoreUtils.js +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -1,59 +1,85 @@ -// Helper: Compare simple values (school, jobTitle) +// Compare simple string values (e.g., institution, location) function compareValues(valueA, valueB) { - return valueA === valueB ? 1 : 0; + if (!valueA || !valueB) return 0; + + const a = + typeof valueA === "string" + ? valueA.trim().toLowerCase() + : String(valueA).toLowerCase(); + const b = + typeof valueB === "string" + ? valueB.trim().toLowerCase() + : String(valueB).toLowerCase(); + + return a === b ? 1 : 0; } -// Helper: Compare tags (intersection similarity) +// Compare array-type tags (e.g., interests, skills) function compareTags(tagsA, tagsB) { - if (!tagsA || !tagsB || !Array.isArray(tagsA) || !Array.isArray(tagsB)) - return 0; + if (!Array.isArray(tagsA) || !Array.isArray(tagsB)) return 0; const setA = new Set(tagsA.map((tag) => tag.toLowerCase())); const setB = new Set(tagsB.map((tag) => tag.toLowerCase())); const intersection = [...setA].filter((tag) => setB.has(tag)); - return intersection.length / Math.max(setA.size, 1); // Avoid division by zero + return intersection.length / Math.max(setA.size, 1); // Avoid div by zero +} + +// Simple deterministic hash function +function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; // Convert to 32-bit integer + } + return hash; +} + +// Stable score variation based on project/user identity +function addStableVariation(score, projectId, userId) { + const combined = `${projectId}_${userId}`; + const hash = simpleHash(combined); + const variation = ((hash % 400) - 200) / 100; // Range: -2 to +2 + return Math.min(100, Math.max(0, score + variation)); } -// Main: Calculate match score for one project and one user +// Main match score calculation function calculateMatchScore(project, user) { - // Weights const weights = { - school: 0.25, - jobTitle: 0.15, - tags: 0.6, + institution: 0.15, + location: 0.05, + interestTags: 0.1, + skillTags: 0.5, + roles: 0.2, }; - const schoolScore = compareValues( - project.institution?.toLowerCase(), - user.college?.toLowerCase() - ); + const institutionScore = compareValues(project.institution, user.college); + const locationScore = compareValues(project.location, user.location); - // Compare against ALL project positions and get best matching role/title - let jobScore = 0; - if (project.positions?.length) { - jobScore = Math.max( + let roleScore = 0; + if (project.positions?.length && Array.isArray(user.roles)) { + roleScore = Math.max( ...project.positions.map((pos) => user.roles.includes(pos.title) ? 1 : 0 ) ); } - // Compare tags: interest_tags and skill_tags const interestScore = compareTags(project.interest_tags, user.interests); const skillScore = compareTags(project.skill_tags, user.skills); - const tagScore = (interestScore + skillScore) / 2; - // Total weighted score const totalScore = - schoolScore * weights.school + - jobScore * weights.jobTitle + - tagScore * weights.tags; + institutionScore * weights.institution + + locationScore * weights.location + + interestScore * weights.interestTags + + skillScore * weights.skillTags + + roleScore * weights.roles; - return Math.round(totalScore * 100); // Normalize to 0–100 + // Apply stable variation + return Math.round(addStableVariation(totalScore * 100, project.id, user.id)); } -// Entry Point: Calculate for all projects +// Entry point export const calculateMatchScores = (userData, projects) => { return projects.map((project) => { const match_percentage = calculateMatchScore(project, userData); From d1d0d9fafc0bee3ddc1a74dd28ab70e03ef8e69a Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 22:04:02 -0700 Subject: [PATCH 15/17] fix: page error on empty tags --- .../src/views/IndividualProjectPage.jsx | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index 2cd000d..3bdec83 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -543,33 +543,39 @@ const ProjectDetailPage = () => {

{project.full_description}

-

Project Categories

- -
- {project.interest_tags.map((interest, index) => ( - - {interest} - - ))} -
- -

Desired Skills

+ {project.interest_tags?.length > 0 && ( +
+

+ Project Categories +

+ {project.interest_tags.map((interest, index) => ( + + {interest} + + ))} +
+ )} -
- {project.skill_tags.map((skill, index) => ( - - {skill} - - ))} -
+ {project.skill_tags?.length > 0 && ( + <> +

Desired Skills

+
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+ + )}
From dc7cce5718297fbd1ec8342745e39c74b65403d2 Mon Sep 17 00:00:00 2001 From: Larry La Date: Sat, 19 Apr 2025 22:14:01 -0700 Subject: [PATCH 16/17] fix: sizing broke --- .../src/views/IndividualProjectPage.jsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index 3bdec83..db61b26 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -544,20 +544,22 @@ const ProjectDetailPage = () => {
{project.interest_tags?.length > 0 && ( -
+ <>

Project Categories

- {project.interest_tags.map((interest, index) => ( - - {interest} - - ))} -
+
+ {project.interest_tags.map((interest, index) => ( + + {interest} + + ))} +
+ )} {project.skill_tags?.length > 0 && ( From a5c24336006fb1004111c068f53b7f7d86858bb4 Mon Sep 17 00:00:00 2001 From: Rebecca Smith <2145912+Rebeccals1@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:24:46 -0700 Subject: [PATCH 17/17] feat: about page (#49) * Updated About page * chore: hide form --------- Co-authored-by: Rebecca Smith <2145912+Rebeccals@users.noreply.github.com> Co-authored-by: Larry La --- frontend/bitmatch/src/App.jsx | 15 +- .../src/components/about/TeamCard.jsx | 41 ++++ frontend/bitmatch/src/components/footer.jsx | 78 -------- frontend/bitmatch/src/components/navbar.jsx | 1 - frontend/bitmatch/src/lib/team_data.js | 33 +++ frontend/bitmatch/src/views/AboutPage.jsx | 189 ++++++++++++++++++ 6 files changed, 277 insertions(+), 80 deletions(-) create mode 100644 frontend/bitmatch/src/components/about/TeamCard.jsx create mode 100644 frontend/bitmatch/src/lib/team_data.js create mode 100644 frontend/bitmatch/src/views/AboutPage.jsx diff --git a/frontend/bitmatch/src/App.jsx b/frontend/bitmatch/src/App.jsx index c3e5c5a..d99e804 100644 --- a/frontend/bitmatch/src/App.jsx +++ b/frontend/bitmatch/src/App.jsx @@ -15,6 +15,7 @@ import ProjectListPage from "./views/ProjectListPage"; import ProjectDetailPage from "./views/IndividualProjectPage"; import AddProjectPage from "./views/AddProjectPage"; import ProfilePage from "./views/ProfilePage"; +import AboutPage from "./views/AboutPage"; import OnboardPage from "./views/OnboardPage"; import InterestPage from "./components/onboarding/Interest"; @@ -33,8 +34,9 @@ function AppRoutes() { const pathname = location.pathname; const isLanding = pathname === "/" && !isSignedIn; const isOnboard = pathname.startsWith("/onboard"); + const isAbout = pathname.startsWith("/about"); - const shouldUseContainer = !isLanding && !isOnboard; + const shouldUseContainer = !isLanding && !isOnboard && !isAbout; const layoutClass = shouldUseContainer ? "container mx-auto px-4 py-16 pb-6 min-h-screen" @@ -145,6 +147,17 @@ function AppRoutes() { } /> + + {/* About */} + + + + } + /> +
diff --git a/frontend/bitmatch/src/components/about/TeamCard.jsx b/frontend/bitmatch/src/components/about/TeamCard.jsx new file mode 100644 index 0000000..09c51c8 --- /dev/null +++ b/frontend/bitmatch/src/components/about/TeamCard.jsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { teamMembers } from "@/lib/team_data"; +import { motion } from "framer-motion"; + +export default function TeamCard() { + const [profiles, setProfiles] = useState([]); + + useEffect(() => { + setProfiles(teamMembers); + }, []); + + return ( +
+ {profiles.map((member, idx) => ( + +
+ {member.name} +
+

+ {member.name} +

+

{member.title}

+
+ ))} +
+ ); +} diff --git a/frontend/bitmatch/src/components/footer.jsx b/frontend/bitmatch/src/components/footer.jsx index 542317a..e51d9a0 100644 --- a/frontend/bitmatch/src/components/footer.jsx +++ b/frontend/bitmatch/src/components/footer.jsx @@ -1,14 +1,6 @@ import { Link } from "react-router-dom"; export default function Footer({ links = [], showSocial = true }) { - const defaultLinks = [ - { href: "/terms", label: "Terms" }, - { href: "/privacy", label: "Privacy" }, - { href: "/contact", label: "Contact" }, - ]; - - const footerLinks = links.length > 0 ? links : defaultLinks; - return (
@@ -17,76 +9,6 @@ export default function Footer({ links = [], showSocial = true }) { © {new Date().getFullYear()} BITMATCH. All rights reserved.

- - - - {showSocial && ( -
- - - - - Facebook - - - - - - - - Instagram - - - - - - Twitter - -
- )}
); diff --git a/frontend/bitmatch/src/components/navbar.jsx b/frontend/bitmatch/src/components/navbar.jsx index c6edeec..c37b4b5 100644 --- a/frontend/bitmatch/src/components/navbar.jsx +++ b/frontend/bitmatch/src/components/navbar.jsx @@ -15,7 +15,6 @@ export default function Navbar({ links = [] }) { const { user } = useUser(); const defaultLinks = [ { href: "/about", label: "About" }, - { href: "/contact", label: "Contact" }, ...(user?.id ? [ { href: "/project-list", label: "Browse" }, diff --git a/frontend/bitmatch/src/lib/team_data.js b/frontend/bitmatch/src/lib/team_data.js new file mode 100644 index 0000000..72d882a --- /dev/null +++ b/frontend/bitmatch/src/lib/team_data.js @@ -0,0 +1,33 @@ +// src/lib/team_data.js + +export const teamMembers = [ + { + id: 1, + name: "Larry La", + title: "Software Engineer", + image: "https://avatars.githubusercontent.com/u/137100338?v=4", + linkedin: "https://www.linkedin.com/in/lqla/", + }, + { + id: 2, + name: "Rebecca Smith", + title: "Frontend Developer", + image: "https://avatars.githubusercontent.com/u/2145912?v=4", + linkedin: "https://www.linkedin.com/in/rebecca-l-smith-3075604/", + }, + { + id: 3, + name: "Luis Dominguez", + title: "Software Engineer", + image: "https://avatars.githubusercontent.com/u/118137779?v=4", + linkedin: "https://www.linkedin.com/in/luis-dominguez-9b11702b5/", + }, + { + id: 4, + name: "William Garcia", + title: "Software Engineer", + image: "https://avatars.githubusercontent.com/u/102125255?v=4", + linkedin: "https://www.linkedin.com/in/william-garcia-946709297/", + }, + ]; + \ No newline at end of file diff --git a/frontend/bitmatch/src/views/AboutPage.jsx b/frontend/bitmatch/src/views/AboutPage.jsx new file mode 100644 index 0000000..2ac328c --- /dev/null +++ b/frontend/bitmatch/src/views/AboutPage.jsx @@ -0,0 +1,189 @@ +import React from "react"; +import { motion } from "framer-motion"; +import TeamCard from "@/components/about/TeamCard"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; + +function ContactItem({ icon: Icon, label, value }) { + return ( +
+ +
+

{label}

+

{value}

+
+
+ ); +} + +export default function AboutPage() { + return ( +
+ {/* Header Section */} + +
+ + About + +
+ + {/* Story Section */} + +

+ BitMatch was created by a team of students from Cal Poly Pomona who + came together under the name Bit by Bit to solve a common challenge: + helping students connect with meaningful project opportunities. +

+

+ Our platform acts as a digital project incubator, where users can + post their ideas, specify skills they need, and discover teammates + with shared interests. Meanwhile, students looking to contribute can + receive intelligent project recommendations based on their profile. +

+

+ Our mission is to foster collaboration, drive innovation, and give + students real-world experience in their fields by enabling them to + build together. +

+
+
+ + {/* Team Section */} + +
+ + The Developers + +

+ Meet the passionate team behind BitMatch. +

+ +
+
+ + {/* Contact Form Section */} + +
+

+ Contact Us +

+

+ Have questions or want to learn more about our mission? Send us a + message—we'd love to hear from you. +

+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +