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/backend/bitmatch/urls.py b/backend/bitmatch/urls.py index 76f97b1..16a7917 100644 --- a/backend/bitmatch/urls.py +++ b/backend/bitmatch/urls.py @@ -21,5 +21,6 @@ urlpatterns = [ path('', views.home), path('admin/', admin.site.urls), - path('projects/', include('projects.urls')) + path('projects/', include('projects.urls')), + path('userauth/', include('userauth.urls')), ] diff --git a/backend/projects/migrations/0001_initial.py b/backend/projects/migrations/0001_initial.py index 7576cf8..bc02e0a 100644 --- a/backend/projects/migrations/0001_initial.py +++ b/backend/projects/migrations/0001_initial.py @@ -1,5 +1,7 @@ -# Generated by Django 5.1.6 on 2025-03-04 06:58 +# Generated by Django 5.1.7 on 2025-04-12 20:52 +import django.contrib.postgres.fields +import uuid from django.db import migrations, models @@ -12,18 +14,39 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Project', + name='Follow', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Like', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('group', models.CharField(max_length=255)), - ('match_percentage', models.FloatField()), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), ('title', models.CharField(max_length=255)), ('institution', models.CharField(max_length=255)), ('description', models.TextField()), - ('followers', models.IntegerField(default=0)), - ('likes', models.IntegerField(default=0)), ('positions', models.JSONField(default=list)), - ('image_url', models.URLField(blank=True, null=True)), + ('image_url', models.ImageField(upload_to='projects/')), + ('group', models.CharField(blank=True, max_length=255, null=True)), + ('followers_count', models.IntegerField(default=0)), + ('likes_count', models.IntegerField(default=0)), + ('match_percentage', models.FloatField(blank=True, null=True)), + ('location', models.JSONField(blank=True, default=list)), + ('images', models.JSONField(blank=True, default=list)), + ('interest_tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None)), + ('skill_tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None)), + ('full_description', models.CharField(blank=True, max_length=1000, null=True)), + ('email', models.EmailField(blank=True, max_length=254, null=True, unique=True)), + ('other_contact', models.CharField(blank=True, max_length=225, null=True)), + ('updates', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=540), blank=True, null=True, size=None)), + ('wanted_description', models.CharField(blank=True, max_length=540, null=True)), ], ), ] diff --git a/backend/projects/migrations/0002_initial.py b/backend/projects/migrations/0002_initial.py new file mode 100644 index 0000000..741feb6 --- /dev/null +++ b/backend/projects/migrations/0002_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 5.1.7 on 2025-04-12 20:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('projects', '0001_initial'), + ('userauth', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='follow', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_follows', to='userauth.user'), + ), + migrations.AddField( + model_name='like', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_likes', to='userauth.user'), + ), + migrations.AddField( + model_name='project', + name='members', + field=models.ManyToManyField(blank=True, related_name='member_of_projects', to='userauth.user'), + ), + migrations.AddField( + model_name='project', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_projects', to='userauth.user'), + ), + migrations.AddField( + model_name='like', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_likes', to='projects.project'), + ), + migrations.AddField( + model_name='follow', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_follows', to='projects.project'), + ), + migrations.AlterUniqueTogether( + name='like', + unique_together={('project', 'user')}, + ), + migrations.AlterUniqueTogether( + name='follow', + unique_together={('project', 'user')}, + ), + ] diff --git a/backend/projects/migrations/0002_project_images.py b/backend/projects/migrations/0002_project_images.py deleted file mode 100644 index a4e2d32..0000000 --- a/backend/projects/migrations/0002_project_images.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-12 07:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='images', - field=models.JSONField(default=list), - ), - ] diff --git a/backend/projects/migrations/0003_rename_images_project_slider_images_and_more.py b/backend/projects/migrations/0003_rename_images_project_slider_images_and_more.py deleted file mode 100644 index 23ade8a..0000000 --- a/backend/projects/migrations/0003_rename_images_project_slider_images_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-16 00:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0002_project_images'), - ] - - operations = [ - migrations.RenameField( - model_name='project', - old_name='images', - new_name='slider_images', - ), - migrations.RemoveField( - model_name='project', - name='image_url', - ), - migrations.AddField( - model_name='project', - name='image', - field=models.ImageField(blank=True, null=True, upload_to='projects/'), - ), - ] diff --git a/backend/projects/migrations/0004_rename_image_project_image_url_and_more.py b/backend/projects/migrations/0004_rename_image_project_image_url_and_more.py deleted file mode 100644 index 01a5257..0000000 --- a/backend/projects/migrations/0004_rename_image_project_image_url_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-16 04:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0003_rename_images_project_slider_images_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='project', - old_name='image', - new_name='image_url', - ), - migrations.RenameField( - model_name='project', - old_name='slider_images', - new_name='images', - ), - ] diff --git a/backend/projects/migrations/0005_rename_followers_project_followers_count_and_more.py b/backend/projects/migrations/0005_rename_followers_project_followers_count_and_more.py deleted file mode 100644 index 33c22cb..0000000 --- a/backend/projects/migrations/0005_rename_followers_project_followers_count_and_more.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-22 02:27 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0004_rename_image_project_image_url_and_more'), - ('userauth', '0001_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='project', - old_name='followers', - new_name='followers_count', - ), - migrations.RenameField( - model_name='project', - old_name='likes', - new_name='likes_count', - ), - migrations.CreateModel( - name='Follow', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_follows', to='projects.project')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_follows', to='userauth.user')), - ], - options={ - 'unique_together': {('project', 'user')}, - }, - ), - migrations.CreateModel( - name='Like', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_likes', to='projects.project')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_likes', to='userauth.user')), - ], - options={ - 'unique_together': {('project', 'user')}, - }, - ), - ] diff --git a/backend/projects/migrations/0006_project_tags.py b/backend/projects/migrations/0006_project_tags.py deleted file mode 100644 index 92d41b4..0000000 --- a/backend/projects/migrations/0006_project_tags.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-25 02:56 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0005_rename_followers_project_followers_count_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='tags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None), - ), - ] diff --git a/backend/projects/migrations/0007_project_email_project_full_description_and_more.py b/backend/projects/migrations/0007_project_email_project_full_description_and_more.py deleted file mode 100644 index 3baed1c..0000000 --- a/backend/projects/migrations/0007_project_email_project_full_description_and_more.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-25 04:35 - -import django.contrib.postgres.fields -import django.db.models.deletion -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0006_project_tags'), - ('userauth', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True, unique=True), - ), - migrations.AddField( - model_name='project', - name='full_description', - field=models.CharField(blank=True, max_length=1000, null=True), - ), - migrations.AddField( - model_name='project', - name='members', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='member_of_projects', to='userauth.user'), - ), - migrations.AddField( - model_name='project', - name='other_contact', - field=models.CharField(blank=True, max_length=225, null=True), - ), - migrations.AddField( - model_name='project', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_projects', to='userauth.user'), - ), - migrations.AddField( - model_name='project', - name='project_id', - field=models.UUIDField(default=uuid.uuid4, editable=False, null=True), - ), - migrations.AddField( - model_name='project', - name='updates', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=540), blank=True, null=True, size=None), - ), - migrations.AddField( - model_name='project', - name='wanted_description', - field=models.CharField(blank=True, max_length=540, null=True), - ), - ] diff --git a/backend/projects/migrations/0008_populate_uuids.py b/backend/projects/migrations/0008_populate_uuids.py deleted file mode 100644 index eda508e..0000000 --- a/backend/projects/migrations/0008_populate_uuids.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-25 04:36 - -from django.db import migrations -import uuid - -def populate_uuids(apps, schema_editor): - Project = apps.get_model('projects', 'Project') - for project in Project.objects.all(): - project.project_id = uuid.uuid4() - project.save() - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0007_project_email_project_full_description_and_more'), - ] - - operations = [ - migrations.RunPython(populate_uuids), - ] diff --git a/backend/projects/migrations/0009_remove_project_id_alter_project_project_id.py b/backend/projects/migrations/0009_remove_project_id_alter_project_project_id.py deleted file mode 100644 index e8d1dd4..0000000 --- a/backend/projects/migrations/0009_remove_project_id_alter_project_project_id.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-25 04:38 - -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0008_populate_uuids'), - ] - - operations = [ - migrations.RemoveField( - model_name='project', - name='id', - ), - migrations.AlterField( - model_name='project', - name='project_id', - field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), - ), - ] diff --git a/backend/projects/migrations/0010_rename_tags_project_interest_tags_project_location_and_more.py b/backend/projects/migrations/0010_rename_tags_project_interest_tags_project_location_and_more.py deleted file mode 100644 index 9f746eb..0000000 --- a/backend/projects/migrations/0010_rename_tags_project_interest_tags_project_location_and_more.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:24 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0009_remove_project_id_alter_project_project_id'), - ] - - operations = [ - migrations.RenameField( - model_name='project', - old_name='tags', - new_name='interest_tags', - ), - migrations.AddField( - model_name='project', - name='location', - field=models.JSONField(blank=True, default=list), - ), - migrations.AddField( - model_name='project', - name='skill_tags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None), - ), - migrations.AlterField( - model_name='project', - name='group', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AlterField( - model_name='project', - name='image_url', - field=models.ImageField(upload_to='projects/'), - ), - migrations.AlterField( - model_name='project', - name='images', - field=models.JSONField(blank=True, default=list), - ), - migrations.AlterField( - model_name='project', - name='match_percentage', - field=models.FloatField(blank=True, null=True), - ), - ] diff --git a/backend/projects/migrations/0011_remove_project_members_project_members.py b/backend/projects/migrations/0011_remove_project_members_project_members.py deleted file mode 100644 index 9fb8480..0000000 --- a/backend/projects/migrations/0011_remove_project_members_project_members.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0010_rename_tags_project_interest_tags_project_location_and_more'), - ('userauth', '0003_rename_tags_user_interest_tags_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='project', - name='members', - ), - migrations.AddField( - model_name='project', - name='members', - field=models.ManyToManyField(blank=True, null=True, related_name='member_of_projects', to='userauth.user'), - ), - ] diff --git a/backend/projects/migrations/0012_alter_project_members.py b/backend/projects/migrations/0012_alter_project_members.py deleted file mode 100644 index 46254bb..0000000 --- a/backend/projects/migrations/0012_alter_project_members.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0011_remove_project_members_project_members'), - ('userauth', '0003_rename_tags_user_interest_tags_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='project', - name='members', - field=models.ManyToManyField(blank=True, related_name='member_of_projects', to='userauth.user'), - ), - ] diff --git a/backend/projects/migrations/0013_rename_project_id_project_id.py b/backend/projects/migrations/0013_rename_project_id_project_id.py deleted file mode 100644 index c77e357..0000000 --- a/backend/projects/migrations/0013_rename_project_id_project_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:32 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0012_alter_project_members'), - ] - - operations = [ - migrations.RenameField( - model_name='project', - old_name='project_id', - new_name='id', - ), - ] diff --git a/backend/projects/models.py b/backend/projects/models.py index eedfb55..681bb20 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -35,6 +35,7 @@ class Project(models.Model): blank=True ) + # todo: see if we can remove this without hurting anything owner = models.ForeignKey( 'userauth.User', on_delete=models.CASCADE, 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') ] diff --git a/backend/userauth/admin.py b/backend/userauth/admin.py index 8aed64c..a4d6528 100644 --- a/backend/userauth/admin.py +++ b/backend/userauth/admin.py @@ -4,7 +4,10 @@ # Register the User model @admin.register(User) class UserAdmin(admin.ModelAdmin): - list_display = ('username', 'email', 'institution', 'followers', 'likes') - search_fields = ('username', 'email', 'institution') - list_filter = ('institution',) - ordering = ('username',) + list_display = ( + 'id', 'auth_id', 'username', 'email', 'first_name', 'last_name', + 'followers', 'likes', 'location' + ) + search_fields = ('username', 'email', 'first_name', 'last_name') + list_filter = ('location',) + filter_horizontal = ('projects', 'owned') diff --git a/backend/userauth/migrations/0001_initial.py b/backend/userauth/migrations/0001_initial.py index 8f7cabd..9007723 100644 --- a/backend/userauth/migrations/0001_initial.py +++ b/backend/userauth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.7 on 2025-03-22 02:27 +# Generated by Django 5.1.7 on 2025-04-12 20:52 import django.contrib.postgres.fields from django.db import migrations, models @@ -9,6 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('projects', '0001_initial'), ] operations = [ @@ -18,12 +19,16 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('auth_id', models.IntegerField(default=999)), ('username', models.CharField(max_length=255, unique=True)), - ('email', models.EmailField(max_length=254, unique=True)), + ('email', models.EmailField(blank=True, max_length=254, null=True, unique=True)), ('institution', models.CharField(blank=True, max_length=255, null=True)), ('followers', models.IntegerField(default=0)), ('likes', models.IntegerField(default=0)), - ('position', models.JSONField(default=list)), - ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None)), + ('positions', models.JSONField(default=list)), + ('interest_tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None)), + ('skill_tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None)), + ('location_preference', models.JSONField(default=list)), + ('owned', models.ManyToManyField(blank=True, related_name='owners', to='projects.project')), + ('projects', models.ManyToManyField(blank=True, related_name='contributors', to='projects.project')), ], ), ] diff --git a/backend/userauth/migrations/0002_rename_interest_tags_user_colleges_and_more.py b/backend/userauth/migrations/0002_rename_interest_tags_user_colleges_and_more.py new file mode 100644 index 0000000..d211924 --- /dev/null +++ b/backend/userauth/migrations/0002_rename_interest_tags_user_colleges_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 5.1.7 on 2025-04-13 22:58 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='interest_tags', + new_name='colleges', + ), + migrations.RenameField( + model_name='user', + old_name='skill_tags', + new_name='interests', + ), + migrations.RenameField( + model_name='user', + old_name='location_preference', + new_name='location_preferences', + ), + migrations.RenameField( + model_name='user', + old_name='positions', + new_name='roles', + ), + migrations.RemoveField( + model_name='user', + name='institution', + ), + migrations.AddField( + model_name='user', + name='first_name', + field=models.CharField(default='', max_length=255), + ), + migrations.AddField( + model_name='user', + name='last_name', + field=models.CharField(default='', max_length=255), + ), + migrations.AddField( + model_name='user', + name='location', + field=models.CharField(default='', max_length=255), + ), + migrations.AddField( + model_name='user', + name='skills', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None), + ), + migrations.AlterField( + model_name='user', + name='auth_id', + field=models.CharField(default=999), + ), + ] diff --git a/backend/userauth/migrations/0002_user_owned_user_projects_alter_user_email.py b/backend/userauth/migrations/0002_user_owned_user_projects_alter_user_email.py deleted file mode 100644 index 1916e25..0000000 --- a/backend/userauth/migrations/0002_user_owned_user_projects_alter_user_email.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-25 04:35 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0007_project_email_project_full_description_and_more'), - ('userauth', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='owned', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owners', to='projects.project'), - ), - migrations.AddField( - model_name='user', - name='projects', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='projects.project'), - ), - migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True, unique=True), - ), - ] diff --git a/backend/userauth/migrations/0003_alter_user_id.py b/backend/userauth/migrations/0003_alter_user_id.py new file mode 100644 index 0000000..e6cab0d --- /dev/null +++ b/backend/userauth/migrations/0003_alter_user_id.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.7 on 2025-04-13 23:04 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0002_rename_interest_tags_user_colleges_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='id', + field=models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False), + ), + ] diff --git a/backend/userauth/migrations/0003_rename_tags_user_interest_tags_and_more.py b/backend/userauth/migrations/0003_rename_tags_user_interest_tags_and_more.py deleted file mode 100644 index 75dc002..0000000 --- a/backend/userauth/migrations/0003_rename_tags_user_interest_tags_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:24 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('userauth', '0002_user_owned_user_projects_alter_user_email'), - ] - - operations = [ - migrations.RenameField( - model_name='user', - old_name='tags', - new_name='interest_tags', - ), - migrations.RenameField( - model_name='user', - old_name='position', - new_name='location_preference', - ), - migrations.AddField( - model_name='user', - name='positions', - field=models.JSONField(default=list), - ), - migrations.AddField( - model_name='user', - name='skill_tags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=225), blank=True, null=True, size=None), - ), - ] diff --git a/backend/userauth/migrations/0004_alter_user_username.py b/backend/userauth/migrations/0004_alter_user_username.py new file mode 100644 index 0000000..ebb8aba --- /dev/null +++ b/backend/userauth/migrations/0004_alter_user_username.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-13 23:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0003_alter_user_id'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(blank=True, max_length=255, null=True, unique=True), + ), + ] diff --git a/backend/userauth/migrations/0004_remove_user_owned_remove_user_projects_user_owned_and_more.py b/backend/userauth/migrations/0004_remove_user_owned_remove_user_projects_user_owned_and_more.py deleted file mode 100644 index e53d9c7..0000000 --- a/backend/userauth/migrations/0004_remove_user_owned_remove_user_projects_user_owned_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 04:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0012_alter_project_members'), - ('userauth', '0003_rename_tags_user_interest_tags_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='owned', - ), - migrations.RemoveField( - model_name='user', - name='projects', - ), - migrations.AddField( - model_name='user', - name='owned', - field=models.ManyToManyField(blank=True, related_name='owners', to='projects.project'), - ), - migrations.AddField( - model_name='user', - name='projects', - field=models.ManyToManyField(blank=True, related_name='contributors', to='projects.project'), - ), - ] diff --git a/backend/userauth/migrations/0005_alter_user_auth_id_alter_user_email_and_more.py b/backend/userauth/migrations/0005_alter_user_auth_id_alter_user_email_and_more.py new file mode 100644 index 0000000..e403ad9 --- /dev/null +++ b/backend/userauth/migrations/0005_alter_user_auth_id_alter_user_email_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.7 on 2025-04-13 23:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0004_alter_user_username'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='auth_id', + field=models.CharField(unique=True), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/backend/userauth/migrations/0006_alter_user_username.py b/backend/userauth/migrations/0006_alter_user_username.py new file mode 100644 index 0000000..2977e6b --- /dev/null +++ b/backend/userauth/migrations/0006_alter_user_username.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-13 23:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0005_alter_user_auth_id_alter_user_email_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(unique=True), + ), + ] diff --git a/backend/userauth/migrations/0007_alter_user_username.py b/backend/userauth/migrations/0007_alter_user_username.py new file mode 100644 index 0000000..cfeeb34 --- /dev/null +++ b/backend/userauth/migrations/0007_alter_user_username.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-14 04:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0006_alter_user_username'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(), + ), + ] diff --git a/backend/userauth/migrations/0008_alter_user_email_alter_user_username.py b/backend/userauth/migrations/0008_alter_user_email_alter_user_username.py new file mode 100644 index 0000000..1f0d700 --- /dev/null +++ b/backend/userauth/migrations/0008_alter_user_email_alter_user_username.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.7 on 2025-04-14 04:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0007_alter_user_username'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True, unique=True), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(blank=True, max_length=150, null=True), + ), + ] diff --git a/backend/userauth/migrations/0009_remove_user_colleges_user_about_me_user_college_and_more.py b/backend/userauth/migrations/0009_remove_user_colleges_user_about_me_user_college_and_more.py new file mode 100644 index 0000000..5e8684c --- /dev/null +++ b/backend/userauth/migrations/0009_remove_user_colleges_user_about_me_user_college_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.7 on 2025-04-16 06:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0008_alter_user_email_alter_user_username'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='colleges', + ), + migrations.AddField( + model_name='user', + name='about_me', + field=models.TextField(blank=True, max_length=500, null=True), + ), + migrations.AddField( + model_name='user', + name='college', + field=models.CharField(default='', max_length=255), + ), + migrations.AddField( + model_name='user', + name='grad_date', + field=models.CharField(default='', max_length=255), + ), + migrations.AddField( + model_name='user', + name='major', + field=models.CharField(default='', max_length=255), + ), + ] diff --git a/backend/userauth/models.py b/backend/userauth/models.py index 979bd35..64d7892 100644 --- a/backend/userauth/models.py +++ b/backend/userauth/models.py @@ -1,17 +1,25 @@ from django.contrib.postgres.fields import ArrayField from django.db import models +import uuid class User(models.Model): - auth_id = models.IntegerField(default=999) - username = models.CharField(max_length=255, unique=True) + id = models.CharField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36) + auth_id = models.CharField(unique=True) + username = models.CharField(max_length=150, blank=True, null=True) email = models.EmailField(unique=True, blank=True, null=True) - institution = models.CharField(max_length=255, blank=True, null=True) + first_name = models.CharField(max_length=255, default="") + last_name = models.CharField(max_length=255, default="") + college = models.CharField(max_length=255, default="") + major = models.CharField(max_length=255, default="") + grad_date = models.CharField(max_length=255, default="") + about_me = models.TextField(max_length=500, blank=True, null=True) followers = models.IntegerField(default=0) likes = models.IntegerField(default=0) - positions = models.JSONField(default=list) - interest_tags = ArrayField(models.CharField(max_length=225), blank=True, null=True) - skill_tags = ArrayField(models.CharField(max_length=225), blank=True, null=True) - location_preference = models.JSONField(default=list) + roles = models.JSONField(default=list) + interests = ArrayField(models.CharField(max_length=225), blank=True, null=True) + skills = ArrayField(models.CharField(max_length=225), blank=True, null=True) + location = models.CharField(max_length=255, default="") + location_preferences = models.JSONField(default=list) projects = models.ManyToManyField( 'projects.Project', @@ -25,4 +33,4 @@ class User(models.Model): ) def __str__(self): - return self.username + return self.username or f"User-{self.id}" diff --git a/backend/userauth/serializers.py b/backend/userauth/serializers.py new file mode 100644 index 0000000..0b3792a --- /dev/null +++ b/backend/userauth/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from .models import User + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = '__all__' \ No newline at end of file diff --git a/backend/userauth/urls.py b/backend/userauth/urls.py new file mode 100644 index 0000000..d474650 --- /dev/null +++ b/backend/userauth/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import UsersCRUDView, check_if_onboarded + +urlpatterns = [ + path('onboard/', UsersCRUDView.as_view(), name='onboard-user'), # Create user + path('onboard/check/', check_if_onboarded, name="onboard-check"), # Check if user onboarded + path('/', UsersCRUDView.as_view(), name='user'), # Read, Update, Delete user by ID +] diff --git a/backend/userauth/views.py b/backend/userauth/views.py index 91ea44a..ec47b4b 100644 --- a/backend/userauth/views.py +++ b/backend/userauth/views.py @@ -1,3 +1,75 @@ -from django.shortcuts import render +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from django.shortcuts import get_object_or_404 +from .models import User +from .serializers import UserSerializer +from rest_framework.permissions import AllowAny +from rest_framework.decorators import api_view, permission_classes + +@api_view(['GET']) +@permission_classes([AllowAny]) +def check_if_onboarded(request, clerk_id): + if User.objects.filter(auth_id=clerk_id).exists(): + return Response({"message": "User already onboarded"}) + else: + return Response({"message": "User not onboarded yet!"}) + +# DRF CRUD views for Users model +class UsersCRUDView(APIView): + permission_classes = [AllowAny] + # Onboard a new user (POST) + def post(self, request): + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({ + "message": "User successfully onboarded", + "data": serializer.data + }, status=status.HTTP_201_CREATED) + else: + print(serializer.errors) + error_details = { + "message": "Invalid user data provided.", + "errors": serializer.errors, + } + return Response(error_details, status=status.HTTP_400_BAD_REQUEST) + + # Get a single user by ID (GET) + def get(self, request, clerk_id): + user = get_object_or_404(User, auth_id=clerk_id) + serializer = UserSerializer(user) + return Response(serializer.data) + + # Update an existing user by ID (PUT) + def put(self, request, clerk_id): + user = get_object_or_404(User, auth_id=clerk_id) + + current_projects = list(user.projects.all()) + + new_projects = request.data.get('projects', []) + + current_project_ids = [project.id for project in current_projects] + + for project in new_projects: + if project not in current_project_ids: + current_project_ids.append(project) + + request.data['projects'] = current_project_ids + + serializer = UserSerializer(user, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response({ + "message": "User successfully updated!", + "data": serializer.data + }, status=status.HTTP_201_CREATED) + else: + print(serializer.errors) + error_details = { + "message": "Invalid user data provided.", + "errors": serializer.errors, + } + return Response(error_details, status=status.HTTP_400_BAD_REQUEST) + -# Create your views here. diff --git a/frontend/bitmatch/eslint.config.js b/frontend/bitmatch/eslint.config.js index 1b66b69..e32436c 100644 --- a/frontend/bitmatch/eslint.config.js +++ b/frontend/bitmatch/eslint.config.js @@ -35,6 +35,7 @@ export default [ { allowConstantExport: true }, ], "react/prop-types": "warn", + "react/display-name": "warn", }, }, ]; 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..d99e804 100644 --- a/frontend/bitmatch/src/App.jsx +++ b/frontend/bitmatch/src/App.jsx @@ -14,88 +14,163 @@ import HomePage from "./views/HomePage"; import ProjectListPage from "./views/ProjectListPage"; import ProjectDetailPage from "./views/IndividualProjectPage"; import AddProjectPage from "./views/AddProjectPage"; -import SignUpPage from "./views/SignUpPage"; -import SignInPage from "./views/SignInPage"; +import ProfilePage from "./views/ProfilePage"; +import AboutPage from "./views/AboutPage"; + 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 isAbout = pathname.startsWith("/about"); + + const shouldUseContainer = !isLanding && !isOnboard && !isAbout; + + const layoutClass = shouldUseContainer + ? "container mx-auto px-4 py-16 pb-6 min-h-screen" + : ""; return ( -
- - {/* Landing or Home */} - - - - + <> + + +
+ + {/* Landing or Home */} + + + + + + + + + } + /> + + {/* Signed-in only routes */} + + + + } + /> + + + + } + /> + - + - - } - /> - - {/* Signed-in only routes */} - - - - } - /> - - - - } - /> - - - - } - /> - - {/* Public pages */} - } /> - } /> - } /> - - {/* Uncomment when ready */} - {/* - } /> - } /> - */} - -
+ } + /> + + + + } + /> + + {/* Onboarding */} + + + + } + > + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + {/* About */} + + + + } + /> + + + + + + ); } +// Default export so main.jsx works export default function App() { return ( - - ); } diff --git a/frontend/bitmatch/src/assets/j-garden2.jpg b/frontend/bitmatch/src/assets/j-garden2.jpg new file mode 100644 index 0000000..525f991 Binary files /dev/null and b/frontend/bitmatch/src/assets/j-garden2.jpg differ diff --git a/frontend/bitmatch/src/assets/profilepic.jpg b/frontend/bitmatch/src/assets/profilepic.jpg new file mode 100644 index 0000000..f31bb92 Binary files /dev/null and b/frontend/bitmatch/src/assets/profilepic.jpg differ 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/landing/HeroSection.jsx b/frontend/bitmatch/src/components/landing/HeroSection.jsx index b5a8b1c..65d2c19 100644 --- a/frontend/bitmatch/src/components/landing/HeroSection.jsx +++ b/frontend/bitmatch/src/components/landing/HeroSection.jsx @@ -1,10 +1,10 @@ import React from "react"; -import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { motion } from "framer-motion"; import groupImg from "../../assets/group_students.png"; import { ArrowRight } from "lucide-react"; import { fadeUp, containerStagger } from "./animations"; +import { SignUpButton } from "@clerk/clerk-react"; export function HeroSection({ user }) { return ( @@ -19,7 +19,10 @@ export function HeroSection({ user }) { >
- + Students collaborating on a project - -

+ +

Build Experience and Community Bit by Bit

- Find relevant roles for job experience or choose a project that’s meaningful, impactful and right for you. + Find relevant roles for job experience or choose a project that’s + meaningful, impactful and right for you.

-
- - - - - +

diff --git a/frontend/bitmatch/src/components/navbar.jsx b/frontend/bitmatch/src/components/navbar.jsx index 677e10f..c37b4b5 100644 --- a/frontend/bitmatch/src/components/navbar.jsx +++ b/frontend/bitmatch/src/components/navbar.jsx @@ -12,13 +12,17 @@ import { Button } from "@/components/ui/button"; import logo from "@/assets/logo.png"; export default function Navbar({ links = [] }) { + const { user } = useUser(); const defaultLinks = [ { href: "/about", label: "About" }, - { href: "/browse", label: "Browse" }, - { href: "/contact", label: "Contact" }, + ...(user?.id + ? [ + { href: "/project-list", label: "Browse" }, + { href: `/profile/${user.id}`, label: "My Profile" }, + ] + : []), ]; - const { user } = useUser(); const [showModal, setShowModal] = useState(false); const [menuOpen, setMenuOpen] = useState(false); diff --git a/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx b/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx new file mode 100644 index 0000000..1feb700 --- /dev/null +++ b/frontend/bitmatch/src/components/onboarding/CreateProfile.jsx @@ -0,0 +1,160 @@ +"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"; + +const CreateProfile = forwardRef(({ onDataChange, formData }, ref) => { + const [firstName, setFirstName] = useState(formData?.first_name || ""); + const [lastName, setLastName] = useState(formData?.last_name || ""); + const [college, setCollege] = useState(formData?.college || ""); + const [major, setMajor] = useState(formData?.major || ""); + const [gradDate, setGradDate] = useState(formData?.grad_date || ""); + const [aboutMe, setAboutMe] = useState(formData?.about_me || ""); + const [errors, setErrors] = useState({}); + + useEffect(() => { + onDataChange({ + first_name: firstName, + last_name: lastName, + college, + major, + grad_date: gradDate, + about_me: aboutMe, + }); + }, [firstName, lastName, college, major, gradDate, aboutMe]); + + useImperativeHandle(ref, () => ({ + validate: () => { + const newErrors = {}; + const monthYearRegex = + /^(January|February|March|April|May|June|July|August|September|October|November|December) \d{4}$/; + + if (!firstName.trim()) newErrors.firstName = "First name is required."; + if (!lastName.trim()) newErrors.lastName = "Last name is required."; + if (!college.trim()) newErrors.college = "College is required."; + if (!major.trim()) newErrors.major = "Major is required."; + if (!gradDate.trim()) { + newErrors.gradDate = "Graduation date is required."; + } else if (!monthYearRegex.test(gradDate.trim())) { + newErrors.gradDate = + 'Graduation date must be in the format "Month YYYY" (e.g. May 2026).'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, + })); + + 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}

+ )} +
+ + {/* College */} +
+ + setCollege(e.target.value)} + placeholder="Enter your college or university" + /> + {errors.college && ( +

{errors.college}

+ )} +
+ + {/* Major */} +
+ + setMajor(e.target.value)} + placeholder="Enter your major" + /> + {errors.major &&

{errors.major}

} +
+ + {/* Graduation Date */} +
+ + setGradDate(e.target.value)} + placeholder="e.g. May 2026" + /> +

+ Format: Month YYYY (e.g. May 2026) +

+ {errors.gradDate && ( +

{errors.gradDate}

+ )} +
+ + {/* About Me (Optional, max 500 chars) */} +
+ + @@ -364,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/HomePage.jsx b/frontend/bitmatch/src/views/HomePage.jsx index eb694c4..3cd1f4c 100644 --- a/frontend/bitmatch/src/views/HomePage.jsx +++ b/frontend/bitmatch/src/views/HomePage.jsx @@ -2,10 +2,47 @@ import { useEffect, useState } from "react"; import axios from "axios"; 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; export default function Home() { + const navigate = useNavigate(); + const { user } = useUser(); + const checkIfUserOnboarded = async (userId) => { + try { + const response = await fetch( + `${SERVER_HOST}/userauth/onboard/check/${userId}`, + { + method: "GET", + } + ); + + if (!response.ok) throw new Error("Failed to check onboarding status"); + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error checking onboarding status:", error); + return null; + } + }; + + useEffect(() => { + const runCheck = async () => { + if (!user) return; + const onboarded = await checkIfUserOnboarded(user.id); + + if (onboarded.message === "User not onboarded yet!") { + navigate("/onboard"); + } + }; + + runCheck(); + }, [user, navigate]); + const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -19,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("Something went wrong. Please try again later."); + } 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) { @@ -56,7 +110,7 @@ export default function Home() { } return ( -

+
diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index a06b050..db61b26 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -10,23 +10,17 @@ 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 } 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 +41,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,6 +53,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 [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 @@ -79,6 +95,48 @@ const ProjectDetailPage = () => { loadProjectInfo(); }, [id]); + const handleFollow = async () => { + setFollowing(!following); + console.log("current following status:", following); + const fdata = { + action: following ? "unfollow" : "follow", + user_id: 1, + }; + console.log("id is ", id); + + try { + const response = await axios.post( + `${SERVER_HOST}/projects/follow/${id}`, + fdata + ); + console.log("response: ", response.data); + window.location.reload(); + } catch (error) { + console.error("Error updating follows: ", error); + } + }; + + const handleLike = async () => { + setLiked(!likeStatus); + console.log("current like status:", likeStatus); + const fdata = { + action: likeStatus ? "unlike" : "like", + user_id: 1, + }; + console.log("id is ", id); + + try { + const response = await axios.post( + `${SERVER_HOST}/projects/like/${id}`, + fdata + ); + console.log("response: ", response.data); + window.location.reload(); + } catch (error) { + console.error("Error updating likes: ", error); + } + }; + const handleSave = async (data) => { console.log("Saving project data:", data); @@ -261,7 +319,7 @@ const ProjectDetailPage = () => { {project.likes_count} Likes
- {project.followers_count} Followers @@ -404,7 +462,11 @@ const ProjectDetailPage = () => { } }} > - + { Contact - { - setIsOpen(true); - e.preventDefault(); - }} - > - Edit Project - + {project.owner === userUuid && ( + { + setIsOpen(true); + e.preventDefault(); + }} + > + Edit Project + + )} {

Overview

-

+

Background & More Details About the Project

{project.full_description}

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

+ Project Categories +

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

Desired Skills

+
+ {project.skill_tags.map((skill, index) => ( + + {skill} + + ))} +
+ + )} +
diff --git a/frontend/bitmatch/src/views/MatchScoreUtils.js b/frontend/bitmatch/src/views/MatchScoreUtils.js new file mode 100644 index 0000000..1c66514 --- /dev/null +++ b/frontend/bitmatch/src/views/MatchScoreUtils.js @@ -0,0 +1,91 @@ +// Compare simple string values (e.g., institution, location) +function compareValues(valueA, valueB) { + 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; +} + +// Compare array-type tags (e.g., interests, skills) +function compareTags(tagsA, tagsB) { + 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 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 match score calculation +function calculateMatchScore(project, user) { + const weights = { + institution: 0.15, + location: 0.05, + interestTags: 0.1, + skillTags: 0.5, + roles: 0.2, + }; + + const institutionScore = compareValues(project.institution, user.college); + const locationScore = compareValues(project.location, user.location); + + 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 + ) + ); + } + + const interestScore = compareTags(project.interest_tags, user.interests); + const skillScore = compareTags(project.skill_tags, user.skills); + + const totalScore = + institutionScore * weights.institution + + locationScore * weights.location + + interestScore * weights.interestTags + + skillScore * weights.skillTags + + roleScore * weights.roles; + + // Apply stable variation + return Math.round(addStableVariation(totalScore * 100, project.id, user.id)); +} + +// Entry point +export const calculateMatchScores = (userData, projects) => { + return projects.map((project) => { + const match_percentage = calculateMatchScore(project, userData); + return { + ...project, + match_percentage, + }; + }); +}; diff --git a/frontend/bitmatch/src/views/OnboardPage.jsx b/frontend/bitmatch/src/views/OnboardPage.jsx index ca532fd..a8d15ee 100644 --- a/frontend/bitmatch/src/views/OnboardPage.jsx +++ b/frontend/bitmatch/src/views/OnboardPage.jsx @@ -1,82 +1,236 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/Label"; -import { Textarea } from "@/components/ui/Textarea"; +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"; +import { useUser } from "@clerk/clerk-react"; +import { useNavigate } from "react-router-dom"; +const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; export default function OnboardPage() { + 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 navigate = useNavigate(); - const [formData, setFormData] = useState({ - role: "", - interests: "", - bio: "", - }); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); + const { user } = useUser(); + const checkIfUserOnboarded = async (userId) => { + try { + const response = await fetch( + `${SERVER_HOST}/userauth/onboard/check/${userId}`, + { + method: "GET", + } + ); + + if (!response.ok) throw new Error("Failed to check onboarding status"); + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error checking onboarding status:", error); + return null; + } }; - const handleSubmit = (e) => { - e.preventDefault(); + useEffect(() => { + const runCheck = async () => { + if (!user) return; + const onboarded = await checkIfUserOnboarded(user.id); - // You can send this data to Supabase, Clerk user metadata, or your backend here - console.log("Onboarding data submitted:", formData); + if (onboarded.message === "User already onboarded") { + alert("You're already onboarded! Redirecting to home..."); + navigate("/"); + } + }; - // Redirect to dashboard or home - navigate("/dashboard"); + runCheck(); + }, [user, navigate]); + + 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 handleNext = () => { + if (validateStep()) { + setCurrentStep(currentStep + 1); + } else { + toast({ + variant: "destructive", + title: "🚫 Incomplete Step", + description: "Please complete this step before proceeding.", + }); + } + }; + + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + + const handleSubmit = async () => { + const onboarded = await checkIfUserOnboarded(user.id); + if (onboarded.message === "User not onboarded yet!") { + try { + const finalFormData = { + ...formData, + auth_id: user.id, + email: user.primaryEmailAddress.emailAddress, + username: user.username, + }; + const res = await fetch(`${SERVER_HOST}/userauth/onboard/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(finalFormData), + }); + if (!res.ok) throw new Error("Failed to submit"); + alert("✅ Successfully onboarded, welcome aboard!"); + navigate("/"); + } catch (err) { + console.error(err); + alert("❌ Onboarding failed, please try again!"); + } + } else { + alert("You have already onboarded, redirecting you to the home page!"); + navigate("/"); + } + }; + + 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. -

- -
-
- - -
- -
- - -
- -
- -