Skip to content

Commit 9bddd62

Browse files
committed
chore: basic leave/join
1 parent 55fc052 commit 9bddd62

9 files changed

Lines changed: 172 additions & 12 deletions

File tree

backend/projects/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Register the Project model
55
@admin.register(Project)
66
class ProjectAdmin(admin.ModelAdmin):
7-
list_display = ('title', 'group', 'institution', 'match_percentage', 'followers_count', 'likes_count')
7+
list_display = ('title', 'group', 'institution', 'followers_count', 'likes_count')
88
search_fields = ('title', 'group', 'institution')
99
list_filter = ('group', 'institution')
1010

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.1.7 on 2025-04-26 03:39
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('projects', '0002_initial'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='project',
15+
name='match_percentage',
16+
),
17+
]

backend/projects/models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class Project(models.Model):
1818
group = models.CharField(max_length=255, blank=True, null=True)
1919
followers_count = models.IntegerField(default=0)
2020
likes_count = models.IntegerField(default=0)
21-
match_percentage = models.FloatField(blank=True, null=True)
2221
location = models.JSONField(default=list, blank=True)
2322
images = models.JSONField(default=list, blank=True)
2423
interest_tags = ArrayField(models.CharField(max_length=225), blank=True, null=True)
@@ -35,7 +34,6 @@ class Project(models.Model):
3534
blank=True
3635
)
3736

38-
# todo: see if we can remove this without hurting anything
3937
owner = models.ForeignKey(
4038
'userauth.User',
4139
on_delete=models.CASCADE,

backend/projects/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from django.urls import path
2-
from .views import ProjectCRUDView, get_projects, toggle_follow, toggle_like
2+
from .views import ProjectCRUDView, get_projects, toggle_follow, toggle_like, project_member_hander
33

44
urlpatterns = [
55
path('', get_projects, name='get_projects'), # Fetch all projects
66
path('create/', ProjectCRUDView.as_view(), name='project-create'), # Create project
77
path('<str:pk>/', ProjectCRUDView.as_view(), name='project-detail'), # Read, Update, Delete project by ID
88
path('like/<str:project_id>', toggle_like, name='toggle_like'),
9-
path('follow/<str:project_id>', toggle_follow, name='toggle_follow')
9+
path('follow/<str:project_id>', toggle_follow, name='toggle_follow'),
10+
path('member/manage/<str:project_id>', project_member_hander, name='project-member-handler'),
1011
]

backend/projects/views.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,36 @@ def toggle_follow(request, project_id):
122122
return Response({'message': 'Project unfollowed successfully'}, status=status.HTTP_200_OK)
123123
else:
124124
return Response({'message': 'You already unfollowed this project.'}, status=status.HTTP_400_BAD_REQUEST)
125+
126+
@api_view(['PATCH'])
127+
@permission_classes([AllowAny])
128+
def project_member_hander(request, project_id):
129+
try:
130+
project = Project.objects.get(id=project_id)
131+
user = User.objects.get(id=request.data['user_id'])
132+
action = request.data.get('action')
133+
134+
is_member = project.members.filter(id=user.id).exists()
135+
136+
if action == "join":
137+
if is_member:
138+
return Response({'error': 'User is already a member of this project'}, status=400)
139+
project.members.add(user)
140+
user.projects.add(project)
141+
return Response({'message': 'Member added successfully'})
142+
143+
elif action == "leave":
144+
if not is_member:
145+
return Response({'error': 'User is not a member of this project'}, status=400)
146+
project.members.remove(user)
147+
user.projects.remove(project)
148+
return Response({'message': 'Member removed successfully'})
149+
150+
else:
151+
return Response({'error': 'Invalid action'}, status=400)
152+
153+
except Project.DoesNotExist:
154+
return Response({'error': 'Project not found'}, status=404)
155+
except User.DoesNotExist:
156+
return Response({'error': 'User not found'}, status=404)
157+

backend/userauth/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ class UserAdmin(admin.ModelAdmin):
1010
)
1111
search_fields = ('username', 'email', 'first_name', 'last_name')
1212
list_filter = ('location',)
13-
filter_horizontal = ('projects', 'owned')
13+
filter_horizontal = ('projects',)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.1.7 on 2025-04-26 03:39
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('userauth', '0009_remove_user_colleges_user_about_me_user_college_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='user',
15+
name='owned',
16+
),
17+
]

backend/userauth/models.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ class User(models.Model):
2626
related_name='contributors',
2727
blank=True
2828
)
29-
owned = models.ManyToManyField(
30-
'projects.Project',
31-
related_name='owners',
32-
blank=True
33-
)
3429

3530
def __str__(self):
3631
return self.username or f"User-{self.id}"

frontend/bitmatch/src/views/IndividualProjectPage.jsx

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
Plus,
55
ThumbsUp,
66
UserRound,
7+
CirclePlus,
8+
LogOut,
79
Star,
810
} from "lucide-react";
911
import { AnimatePresence, motion } from "framer-motion";
@@ -63,6 +65,8 @@ const ProjectDetailPage = () => {
6365
const [likeStatus, setLiked] = useState(false);
6466
const [userUuid, setUserUuid] = useState(null);
6567
const [cooldown, setCooldown] = useState(false);
68+
const [isMember, setIsMember] = useState(false);
69+
const [isReady, setIsReady] = useState(false);
6670

6771
useEffect(() => {
6872
const fetchUserUuid = async () => {
@@ -102,7 +106,14 @@ const ProjectDetailPage = () => {
102106
};
103107

104108
loadProjectInfo();
105-
}, [id]);
109+
}, [id, userUuid]);
110+
111+
useEffect(() => {
112+
if (userData && project) {
113+
setIsMember(project.members.includes(userData.id));
114+
setIsReady(true);
115+
}
116+
}, [userData, project]);
106117

107118
const handleFollow = async () => {
108119
setFollowing(!following);
@@ -125,6 +136,62 @@ const ProjectDetailPage = () => {
125136
}
126137
};
127138

139+
const handleJoin = async () => {
140+
const confirmed = window.confirm(
141+
"Are you sure you want to join this project?"
142+
);
143+
if (!confirmed) return;
144+
try {
145+
const response = await fetch(
146+
`${SERVER_HOST}/projects/member/manage/${id}`,
147+
{
148+
method: "PATCH",
149+
headers: {
150+
"Content-Type": "application/json",
151+
},
152+
body: JSON.stringify({
153+
user_id: userData.id,
154+
action: "join",
155+
}),
156+
}
157+
);
158+
159+
if (response.ok) {
160+
setIsMember(true);
161+
}
162+
} catch (error) {
163+
console.error("Error joining project:", error);
164+
}
165+
};
166+
167+
const handleLeave = async () => {
168+
const confirmed = window.confirm(
169+
"Are you sure you want to leave this project?"
170+
);
171+
if (!confirmed) return;
172+
try {
173+
const response = await fetch(
174+
`${SERVER_HOST}/projects/member/manage/${id}`,
175+
{
176+
method: "PATCH",
177+
headers: {
178+
"Content-Type": "application/json",
179+
},
180+
body: JSON.stringify({
181+
user_id: userData.id,
182+
action: "leave",
183+
}),
184+
}
185+
);
186+
187+
if (response.ok) {
188+
setIsMember(false);
189+
}
190+
} catch (error) {
191+
console.error("Error leaving project:", error);
192+
}
193+
};
194+
128195
const ai_feedback = async () => {
129196
if (cooldown) return;
130197
setCooldown(true);
@@ -463,6 +530,38 @@ const ProjectDetailPage = () => {
463530
</Button>
464531
<span>{project.followers_count} Followers</span>
465532
</div>
533+
534+
<div className="flex items-center gap-2">
535+
{isReady && userUuid !== project.owner && (
536+
<>
537+
<Button
538+
variant="ghost"
539+
size="icon"
540+
onClick={isMember ? handleLeave : handleJoin}
541+
className={
542+
isMember
543+
? "bg-red-500 hover:bg-red-100"
544+
: "bg-green-500 hover:bg-green-600"
545+
}
546+
>
547+
{isMember ? (
548+
<LogOut className="h-6 w-6" />
549+
) : (
550+
<CirclePlus className="h-6 w-6" />
551+
)}
552+
</Button>
553+
<span
554+
className={
555+
isMember
556+
? "text-red-600 font-bold"
557+
: "text-green-500 font-bold"
558+
}
559+
>
560+
{isMember ? "Leave" : "Join"}
561+
</span>
562+
</>
563+
)}
564+
</div>
466565
</div>
467566
</div>
468567
</div>

0 commit comments

Comments
 (0)