diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc index 66e390eb..3e691e69 100644 Binary files a/backend/app/__pycache__/__init__.cpython-312.pyc and b/backend/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/__pycache__/dependencies.cpython-312.pyc b/backend/app/__pycache__/dependencies.cpython-312.pyc new file mode 100644 index 00000000..60c4ba42 Binary files /dev/null and b/backend/app/__pycache__/dependencies.cpython-312.pyc differ diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 00000000..d36d02b5 Binary files /dev/null and b/backend/app/__pycache__/main.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/__init__.cpython-312.pyc b/backend/app/core/__pycache__/__init__.cpython-312.pyc index e0b1f44f..a7e22260 100644 Binary files a/backend/app/core/__pycache__/__init__.cpython-312.pyc and b/backend/app/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index dd81e831..4eb59219 100644 Binary files a/backend/app/core/__pycache__/config.cpython-312.pyc and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/logging.cpython-312.pyc b/backend/app/core/__pycache__/logging.cpython-312.pyc index cd6c69f7..9b066424 100644 Binary files a/backend/app/core/__pycache__/logging.cpython-312.pyc and b/backend/app/core/__pycache__/logging.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc index c8f18e90..f7c049be 100644 Binary files a/backend/app/core/__pycache__/security.cpython-312.pyc and b/backend/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/database/__pycache__/__init__.cpython-312.pyc b/backend/app/database/__pycache__/__init__.cpython-312.pyc index 14ac7785..4435d43b 100644 Binary files a/backend/app/database/__pycache__/__init__.cpython-312.pyc and b/backend/app/database/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/database/__pycache__/base.cpython-312.pyc b/backend/app/database/__pycache__/base.cpython-312.pyc index ad4deebc..2a82ddeb 100644 Binary files a/backend/app/database/__pycache__/base.cpython-312.pyc and b/backend/app/database/__pycache__/base.cpython-312.pyc differ diff --git a/backend/app/database/__pycache__/database.cpython-312.pyc b/backend/app/database/__pycache__/database.cpython-312.pyc index ffa69707..fb019bbc 100644 Binary files a/backend/app/database/__pycache__/database.cpython-312.pyc and b/backend/app/database/__pycache__/database.cpython-312.pyc differ diff --git a/backend/app/database/__pycache__/session.cpython-312.pyc b/backend/app/database/__pycache__/session.cpython-312.pyc index aac40166..3bfbd0f6 100644 Binary files a/backend/app/database/__pycache__/session.cpython-312.pyc and b/backend/app/database/__pycache__/session.cpython-312.pyc differ diff --git a/backend/app/middleware/__pycache__/__init__.cpython-312.pyc b/backend/app/middleware/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..5c0897fa Binary files /dev/null and b/backend/app/middleware/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/middleware/__pycache__/rate_limit.cpython-312.pyc b/backend/app/middleware/__pycache__/rate_limit.cpython-312.pyc new file mode 100644 index 00000000..4e20d0b2 Binary files /dev/null and b/backend/app/middleware/__pycache__/rate_limit.cpython-312.pyc differ diff --git a/backend/app/middleware/__pycache__/request_id.cpython-312.pyc b/backend/app/middleware/__pycache__/request_id.cpython-312.pyc new file mode 100644 index 00000000..18fd8c19 Binary files /dev/null and b/backend/app/middleware/__pycache__/request_id.cpython-312.pyc differ diff --git a/backend/app/middleware/__pycache__/security_headers.cpython-312.pyc b/backend/app/middleware/__pycache__/security_headers.cpython-312.pyc new file mode 100644 index 00000000..b3c53468 Binary files /dev/null and b/backend/app/middleware/__pycache__/security_headers.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index b7ebbeb7..50aeba43 100644 Binary files a/backend/app/models/__pycache__/__init__.cpython-312.pyc and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/activity.cpython-312.pyc b/backend/app/models/__pycache__/activity.cpython-312.pyc index a04906d8..576c99ab 100644 Binary files a/backend/app/models/__pycache__/activity.cpython-312.pyc and b/backend/app/models/__pycache__/activity.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/application.cpython-312.pyc b/backend/app/models/__pycache__/application.cpython-312.pyc index 5a0478bb..32c4dd70 100644 Binary files a/backend/app/models/__pycache__/application.cpython-312.pyc and b/backend/app/models/__pycache__/application.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/audit_log.cpython-312.pyc b/backend/app/models/__pycache__/audit_log.cpython-312.pyc index 7d503c7d..5ba8103f 100644 Binary files a/backend/app/models/__pycache__/audit_log.cpython-312.pyc and b/backend/app/models/__pycache__/audit_log.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/bookmark.cpython-312.pyc b/backend/app/models/__pycache__/bookmark.cpython-312.pyc index f85b4745..793d039d 100644 Binary files a/backend/app/models/__pycache__/bookmark.cpython-312.pyc and b/backend/app/models/__pycache__/bookmark.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/builder_flare.cpython-312.pyc b/backend/app/models/__pycache__/builder_flare.cpython-312.pyc index 685463cb..b05d01b7 100644 Binary files a/backend/app/models/__pycache__/builder_flare.cpython-312.pyc and b/backend/app/models/__pycache__/builder_flare.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/conversation.cpython-312.pyc b/backend/app/models/__pycache__/conversation.cpython-312.pyc index c7d98bff..19ee20b3 100644 Binary files a/backend/app/models/__pycache__/conversation.cpython-312.pyc and b/backend/app/models/__pycache__/conversation.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/conversation_member.cpython-312.pyc b/backend/app/models/__pycache__/conversation_member.cpython-312.pyc index 6217164a..ce850157 100644 Binary files a/backend/app/models/__pycache__/conversation_member.cpython-312.pyc and b/backend/app/models/__pycache__/conversation_member.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/follower.cpython-312.pyc b/backend/app/models/__pycache__/follower.cpython-312.pyc index 6ff2650f..676b2864 100644 Binary files a/backend/app/models/__pycache__/follower.cpython-312.pyc and b/backend/app/models/__pycache__/follower.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/message.cpython-312.pyc b/backend/app/models/__pycache__/message.cpython-312.pyc index 5653dc9a..8ff15fbb 100644 Binary files a/backend/app/models/__pycache__/message.cpython-312.pyc and b/backend/app/models/__pycache__/message.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/notification.cpython-312.pyc b/backend/app/models/__pycache__/notification.cpython-312.pyc index 1f6329d2..c40d5390 100644 Binary files a/backend/app/models/__pycache__/notification.cpython-312.pyc and b/backend/app/models/__pycache__/notification.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index c37dae95..c24b05dd 100644 Binary files a/backend/app/models/__pycache__/organization.cpython-312.pyc and b/backend/app/models/__pycache__/organization.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/project.cpython-312.pyc b/backend/app/models/__pycache__/project.cpython-312.pyc index cc8f6e44..90c01178 100644 Binary files a/backend/app/models/__pycache__/project.cpython-312.pyc and b/backend/app/models/__pycache__/project.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/project_member.cpython-312.pyc b/backend/app/models/__pycache__/project_member.cpython-312.pyc index a01da99d..59804655 100644 Binary files a/backend/app/models/__pycache__/project_member.cpython-312.pyc and b/backend/app/models/__pycache__/project_member.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/project_skill.cpython-312.pyc b/backend/app/models/__pycache__/project_skill.cpython-312.pyc index aa25e5f4..b428ff56 100644 Binary files a/backend/app/models/__pycache__/project_skill.cpython-312.pyc and b/backend/app/models/__pycache__/project_skill.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/refresh_token.cpython-312.pyc b/backend/app/models/__pycache__/refresh_token.cpython-312.pyc index 2685a7c4..2e785180 100644 Binary files a/backend/app/models/__pycache__/refresh_token.cpython-312.pyc and b/backend/app/models/__pycache__/refresh_token.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/repository.cpython-312.pyc b/backend/app/models/__pycache__/repository.cpython-312.pyc index 0b806412..f500c6d7 100644 Binary files a/backend/app/models/__pycache__/repository.cpython-312.pyc and b/backend/app/models/__pycache__/repository.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/skill.cpython-312.pyc b/backend/app/models/__pycache__/skill.cpython-312.pyc index e08e6fb2..9b367843 100644 Binary files a/backend/app/models/__pycache__/skill.cpython-312.pyc and b/backend/app/models/__pycache__/skill.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/user.cpython-312.pyc b/backend/app/models/__pycache__/user.cpython-312.pyc index 77ef6b60..c878084a 100644 Binary files a/backend/app/models/__pycache__/user.cpython-312.pyc and b/backend/app/models/__pycache__/user.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/user_skill.cpython-312.pyc b/backend/app/models/__pycache__/user_skill.cpython-312.pyc index 23de3730..e03cb203 100644 Binary files a/backend/app/models/__pycache__/user_skill.cpython-312.pyc and b/backend/app/models/__pycache__/user_skill.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/__init__.cpython-312.pyc b/backend/app/routers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..11bcb54a Binary files /dev/null and b/backend/app/routers/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/activities.cpython-312.pyc b/backend/app/routers/__pycache__/activities.cpython-312.pyc new file mode 100644 index 00000000..fdc28b1f Binary files /dev/null and b/backend/app/routers/__pycache__/activities.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/applications.cpython-312.pyc b/backend/app/routers/__pycache__/applications.cpython-312.pyc new file mode 100644 index 00000000..62c05cf4 Binary files /dev/null and b/backend/app/routers/__pycache__/applications.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/auth.cpython-312.pyc b/backend/app/routers/__pycache__/auth.cpython-312.pyc new file mode 100644 index 00000000..6706c245 Binary files /dev/null and b/backend/app/routers/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/bookmarks.cpython-312.pyc b/backend/app/routers/__pycache__/bookmarks.cpython-312.pyc new file mode 100644 index 00000000..fc391d61 Binary files /dev/null and b/backend/app/routers/__pycache__/bookmarks.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/builder_flares.cpython-312.pyc b/backend/app/routers/__pycache__/builder_flares.cpython-312.pyc new file mode 100644 index 00000000..cbd14e30 Binary files /dev/null and b/backend/app/routers/__pycache__/builder_flares.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/conversations.cpython-312.pyc b/backend/app/routers/__pycache__/conversations.cpython-312.pyc new file mode 100644 index 00000000..52db38f0 Binary files /dev/null and b/backend/app/routers/__pycache__/conversations.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/followers.cpython-312.pyc b/backend/app/routers/__pycache__/followers.cpython-312.pyc new file mode 100644 index 00000000..bd651ff1 Binary files /dev/null and b/backend/app/routers/__pycache__/followers.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/messages.cpython-312.pyc b/backend/app/routers/__pycache__/messages.cpython-312.pyc new file mode 100644 index 00000000..16d3b9c9 Binary files /dev/null and b/backend/app/routers/__pycache__/messages.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/notifications.cpython-312.pyc b/backend/app/routers/__pycache__/notifications.cpython-312.pyc new file mode 100644 index 00000000..6e95df41 Binary files /dev/null and b/backend/app/routers/__pycache__/notifications.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/organizations.cpython-312.pyc b/backend/app/routers/__pycache__/organizations.cpython-312.pyc new file mode 100644 index 00000000..a07969d8 Binary files /dev/null and b/backend/app/routers/__pycache__/organizations.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/projects.cpython-312.pyc b/backend/app/routers/__pycache__/projects.cpython-312.pyc new file mode 100644 index 00000000..a2cc2329 Binary files /dev/null and b/backend/app/routers/__pycache__/projects.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/repositories.cpython-312.pyc b/backend/app/routers/__pycache__/repositories.cpython-312.pyc new file mode 100644 index 00000000..60766535 Binary files /dev/null and b/backend/app/routers/__pycache__/repositories.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/skills.cpython-312.pyc b/backend/app/routers/__pycache__/skills.cpython-312.pyc new file mode 100644 index 00000000..6a1cfc37 Binary files /dev/null and b/backend/app/routers/__pycache__/skills.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/users.cpython-312.pyc b/backend/app/routers/__pycache__/users.cpython-312.pyc new file mode 100644 index 00000000..929cbb9b Binary files /dev/null and b/backend/app/routers/__pycache__/users.cpython-312.pyc differ diff --git a/backend/app/routers/followers.py b/backend/app/routers/followers.py index 91ab3341..84589a0c 100644 --- a/backend/app/routers/followers.py +++ b/backend/app/routers/followers.py @@ -10,6 +10,8 @@ from app.models.user import User from app.schemas.follower import FollowerResponse from app.services.follower_service import FollowerService +from app.models.notification import NotificationType +from app.services.notification_service import NotificationService router = APIRouter( prefix="/followers", @@ -46,12 +48,27 @@ def follow_user( detail="Already following this user", ) - return FollowerService.follow_user( + follow = FollowerService.follow_user( db, current_user.id, user_id, ) + try: + NotificationService.enqueue( + db, + recipient_id=user_id, + sender_id=current_user.id, + type=NotificationType.FOLLOW, + title="New follower", + message=f"{current_user.username} started following you.", + action_url=f"/users/{current_user.id}", + ) + except Exception: + db.rollback() + + return follow + @router.delete( "/{user_id}", diff --git a/backend/app/schemas/__pycache__/__init__.cpython-312.pyc b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..e6a3d83d Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/activity.cpython-312.pyc b/backend/app/schemas/__pycache__/activity.cpython-312.pyc new file mode 100644 index 00000000..d2aab139 Binary files /dev/null and b/backend/app/schemas/__pycache__/activity.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc new file mode 100644 index 00000000..80c62eba Binary files /dev/null and b/backend/app/schemas/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/follower.cpython-312.pyc b/backend/app/schemas/__pycache__/follower.cpython-312.pyc new file mode 100644 index 00000000..f7a95180 Binary files /dev/null and b/backend/app/schemas/__pycache__/follower.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc new file mode 100644 index 00000000..a5f7d280 Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-312.pyc differ diff --git a/backend/app/schemas/follower.py b/backend/app/schemas/follower.py new file mode 100644 index 00000000..1dac4765 --- /dev/null +++ b/backend/app/schemas/follower.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import uuid +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + + +# ========================================================== +# Follower Response +# ========================================================== + + +class FollowerResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID + follower_id: uuid.UUID + following_id: uuid.UUID + created_at: datetime + + +# ========================================================== +# Follow Status +# ========================================================== + + +class FollowStatusResponse(BaseModel): + is_following: bool + follower_count: int + following_count: int + + +# ========================================================== +# Follow Action Response +# ========================================================== + + +class FollowActionResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID + follower_id: uuid.UUID + following_id: uuid.UUID + created_at: datetime + follower_count: int + following_count: int + + +# ========================================================== +# Unfollow Response +# ========================================================== + + +class UnfollowResponse(BaseModel): + message: str + follower_count: int + following_count: int diff --git a/backend/app/services/__pycache__/__init__.cpython-312.pyc b/backend/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..59719c6b Binary files /dev/null and b/backend/app/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/activity_service.cpython-312.pyc b/backend/app/services/__pycache__/activity_service.cpython-312.pyc new file mode 100644 index 00000000..ab34ff04 Binary files /dev/null and b/backend/app/services/__pycache__/activity_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/application_service.cpython-312.pyc b/backend/app/services/__pycache__/application_service.cpython-312.pyc new file mode 100644 index 00000000..3a417af2 Binary files /dev/null and b/backend/app/services/__pycache__/application_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/audit_log_service.cpython-312.pyc b/backend/app/services/__pycache__/audit_log_service.cpython-312.pyc new file mode 100644 index 00000000..80d51c13 Binary files /dev/null and b/backend/app/services/__pycache__/audit_log_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc new file mode 100644 index 00000000..190fb577 Binary files /dev/null and b/backend/app/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/bookmark_service.cpython-312.pyc b/backend/app/services/__pycache__/bookmark_service.cpython-312.pyc new file mode 100644 index 00000000..252f0259 Binary files /dev/null and b/backend/app/services/__pycache__/bookmark_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/builder_flare_service.cpython-312.pyc b/backend/app/services/__pycache__/builder_flare_service.cpython-312.pyc new file mode 100644 index 00000000..7acece43 Binary files /dev/null and b/backend/app/services/__pycache__/builder_flare_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/conversation_service.cpython-312.pyc b/backend/app/services/__pycache__/conversation_service.cpython-312.pyc new file mode 100644 index 00000000..9594ddbd Binary files /dev/null and b/backend/app/services/__pycache__/conversation_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/follower_service.cpython-312.pyc b/backend/app/services/__pycache__/follower_service.cpython-312.pyc new file mode 100644 index 00000000..47f3ca4f Binary files /dev/null and b/backend/app/services/__pycache__/follower_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/message_service.cpython-312.pyc b/backend/app/services/__pycache__/message_service.cpython-312.pyc new file mode 100644 index 00000000..306058c8 Binary files /dev/null and b/backend/app/services/__pycache__/message_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/notification_service.cpython-312.pyc b/backend/app/services/__pycache__/notification_service.cpython-312.pyc new file mode 100644 index 00000000..a1fb42a4 Binary files /dev/null and b/backend/app/services/__pycache__/notification_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/organization_service.cpython-312.pyc b/backend/app/services/__pycache__/organization_service.cpython-312.pyc new file mode 100644 index 00000000..a4a2ec07 Binary files /dev/null and b/backend/app/services/__pycache__/organization_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/project_service.cpython-312.pyc b/backend/app/services/__pycache__/project_service.cpython-312.pyc new file mode 100644 index 00000000..9f16593c Binary files /dev/null and b/backend/app/services/__pycache__/project_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/refresh_token_service.cpython-312.pyc b/backend/app/services/__pycache__/refresh_token_service.cpython-312.pyc new file mode 100644 index 00000000..4af5f939 Binary files /dev/null and b/backend/app/services/__pycache__/refresh_token_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/repository_service.cpython-312.pyc b/backend/app/services/__pycache__/repository_service.cpython-312.pyc new file mode 100644 index 00000000..bc596468 Binary files /dev/null and b/backend/app/services/__pycache__/repository_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/skill_service.cpython-312.pyc b/backend/app/services/__pycache__/skill_service.cpython-312.pyc new file mode 100644 index 00000000..5f9b12d7 Binary files /dev/null and b/backend/app/services/__pycache__/skill_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/user_service.cpython-312.pyc b/backend/app/services/__pycache__/user_service.cpython-312.pyc new file mode 100644 index 00000000..4a9518b6 Binary files /dev/null and b/backend/app/services/__pycache__/user_service.cpython-312.pyc differ diff --git a/backend/app/utils/__pycache__/__init__.cpython-312.pyc b/backend/app/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..adc91fbe Binary files /dev/null and b/backend/app/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/utils/__pycache__/validators.cpython-312.pyc b/backend/app/utils/__pycache__/validators.cpython-312.pyc new file mode 100644 index 00000000..4279f6bc Binary files /dev/null and b/backend/app/utils/__pycache__/validators.cpython-312.pyc differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7bdbe075..d11e36d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7711,18 +7711,6 @@ } } }, - "node_modules/nitro/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nitro/node_modules/unstorage": { "version": "2.0.0-alpha.7", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz", diff --git a/frontend/src/components/shared/FollowButton.tsx b/frontend/src/components/shared/FollowButton.tsx new file mode 100644 index 00000000..744260cf --- /dev/null +++ b/frontend/src/components/shared/FollowButton.tsx @@ -0,0 +1,86 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useFollowStatus, useFollow, useUnfollow } from "@/hooks/useFollow"; +import { Loader2, UserPlus, UserCheck, UserMinus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { UUID } from "@/lib/api"; + +export function FollowButton({ + userId, + className, +}: { + userId: UUID; + className?: string; +}) { + const { data: status, isLoading: statusLoading } = useFollowStatus(userId); + const followMutation = useFollow(userId); + const unfollowMutation = useUnfollow(userId); + + const [hovered, setHovered] = useState(false); + + const isFollowing = status?.is_following ?? false; + const isBusy = followMutation.isPending || unfollowMutation.isPending; + + function handleClick() { + if (isBusy) return; + if (isFollowing) { + unfollowMutation.mutate(); + } else { + followMutation.mutate(); + } + } + + if (statusLoading) { + return ( + + ); + } + + if (isFollowing) { + return ( + + ); + } + + return ( + + ); +} diff --git a/frontend/src/features/dashboard/sections.tsx b/frontend/src/features/dashboard/sections.tsx index 3d82a180..09b5335f 100644 --- a/frontend/src/features/dashboard/sections.tsx +++ b/frontend/src/features/dashboard/sections.tsx @@ -26,6 +26,7 @@ import { Users2, FileText, BarChart3, + Trophy, } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { cn } from "@/lib/utils"; diff --git a/frontend/src/hooks/useFollow.ts b/frontend/src/hooks/useFollow.ts new file mode 100644 index 00000000..7c937b26 --- /dev/null +++ b/frontend/src/hooks/useFollow.ts @@ -0,0 +1,112 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + followUser, + unfollowUser, + getFollowStatus, + toastError, + type UUID, + type FollowStatusResponse, +} from "@/lib/api"; + +export function useFollowStatus(userId: UUID | undefined) { + return useQuery({ + queryKey: ["follow-status", userId], + queryFn: () => getFollowStatus(userId!), + enabled: !!userId, + }); +} + +export function useFollow(userId: UUID) { + const queryClient = useQueryClient(); + const queryKey = ["follow-status", userId]; + + return useMutation({ + mutationFn: () => followUser(userId), + + onMutate: async () => { + await queryClient.cancelQueries({ queryKey }); + + const previous = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (old) => + old + ? { + ...old, + is_following: true, + follower_count: old.follower_count + 1, + } + : { is_following: true, follower_count: 1, following_count: 0 }, + ); + + return { previous }; + }, + + onError: (err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData(queryKey, context.previous); + } + toastError(err, "Failed to follow"); + }, + + onSuccess: (data) => { + queryClient.setQueryData(queryKey, { + is_following: true, + follower_count: data.follower_count, + following_count: data.following_count, + }); + toast.success("Followed successfully"); + }, + + onSettled: () => { + queryClient.invalidateQueries({ queryKey }); + }, + }); +} + +export function useUnfollow(userId: UUID) { + const queryClient = useQueryClient(); + const queryKey = ["follow-status", userId]; + + return useMutation({ + mutationFn: () => unfollowUser(userId), + + onMutate: async () => { + await queryClient.cancelQueries({ queryKey }); + + const previous = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (old) => + old + ? { + ...old, + is_following: false, + follower_count: Math.max(0, old.follower_count - 1), + } + : { is_following: false, follower_count: 0, following_count: 0 }, + ); + + return { previous }; + }, + + onError: (err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData(queryKey, context.previous); + } + toastError(err, "Failed to unfollow"); + }, + + onSuccess: (data) => { + queryClient.setQueryData(queryKey, { + is_following: false, + follower_count: data.follower_count, + following_count: data.following_count, + }); + toast.success("Unfollowed successfully"); + }, + + onSettled: () => { + queryClient.invalidateQueries({ queryKey }); + }, + }); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4dd18b3d..f469a0f0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -3,12 +3,7 @@ import { toast } from "sonner"; type JsonPrimitive = string | number | boolean | null; type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; -export type ApplicationStatus = - | "pending" - | "reviewing" - | "accepted" - | "rejected" - | "withdrawn"; +export type ApplicationStatus = "pending" | "reviewing" | "accepted" | "rejected" | "withdrawn"; export type UUID = string; @@ -63,13 +58,11 @@ function assertJson(data: unknown): T { return data as T; } -async function requestJson( - input: { - url: string; - method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; - body?: TBody; - }, -): Promise { +async function requestJson(input: { + url: string; + method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + body?: TBody; +}): Promise { const { baseUrl } = getApiConfig(); const res = await fetch(`${baseUrl}${input.url}`, { @@ -167,4 +160,3 @@ export function toastError(err: unknown, fallback = "Something went wrong") { const message = err instanceof Error ? err.message : fallback; toast.error(message); } - diff --git a/frontend/src/routes/_app.builders.$builderId.tsx b/frontend/src/routes/_app.builders.$builderId.tsx index b8dbe26f..a5c98c6b 100644 --- a/frontend/src/routes/_app.builders.$builderId.tsx +++ b/frontend/src/routes/_app.builders.$builderId.tsx @@ -2,7 +2,8 @@ import { createFileRoute, Link, notFound } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; import { buildersService } from "@/services"; import { Card, TagChip, Avatar } from "@/components/shared/primitives"; -import { ArrowLeft, MessageSquare, UserPlus } from "lucide-react"; +import { FollowButton } from "@/components/shared/FollowButton"; +import { ArrowLeft, MessageSquare } from "lucide-react"; import { BackButton } from "@/components/shared/BackButton"; export const Route = createFileRoute("/_app/builders/$builderId")({ @@ -35,9 +36,7 @@ function BuilderProfile() {
- + diff --git a/frontend/src/routes/_app.profile.$username.tsx b/frontend/src/routes/_app.profile.$username.tsx index 25b0bb0c..a48b9ae0 100644 --- a/frontend/src/routes/_app.profile.$username.tsx +++ b/frontend/src/routes/_app.profile.$username.tsx @@ -7,7 +7,10 @@ export const Route = createFileRoute("/_app/profile/$username")({ head: ({ params }) => ({ meta: [ { title: `@${params.username} — DevLink` }, - { name: "description", content: `${params.username}'s DevLink profile: skills, projects and activity.` }, + { + name: "description", + content: `${params.username}'s DevLink profile: skills, projects and activity.`, + }, ], }), component: ProfilePage, @@ -17,7 +20,14 @@ function ProfilePage() { const { username } = Route.useParams(); const me = username === currentUser.handle; const b = me - ? { ...builders[0], name: currentUser.name, handle: currentUser.handle, avatar: currentUser.avatar, bio: "Product engineer. Ships fast, sleeps sometimes.", role: "Full Stack Developer" } + ? { + ...builders[0], + name: currentUser.name, + handle: currentUser.handle, + avatar: currentUser.avatar, + bio: "Product engineer. Ships fast, sleeps sometimes.", + role: "Full Stack Developer", + } : builders.find((x) => x.handle === username); if (!b) throw notFound(); @@ -28,12 +38,20 @@ function ProfilePage() {

{b.name}

-

@{b.handle} · {b.role}

+

+ @{b.handle} · {b.role} +

{b.bio}

- {b.country} - Joined 2024 - devlink.io/{b.handle} + + {b.country} + + + Joined 2024 + + + devlink.io/{b.handle} +
{!me && ( @@ -48,7 +66,9 @@ function ProfilePage() {

Skills

- {b.skills.map((s) => {s})} + {b.skills.map((s) => ( + {s} + ))}
@@ -56,10 +76,14 @@ function ProfilePage() {
    {projects.slice(0, 4).map((p) => (
  • - {p.icon} + + {p.icon} +

    {p.name}

    -

    {p.stack.join(" · ")}

    +

    + {p.stack.join(" · ")} +

  • ))} diff --git a/frontend/src/routes/_app.projects.$projectId.tsx b/frontend/src/routes/_app.projects.$projectId.tsx index 6ef81a33..209971e7 100644 --- a/frontend/src/routes/_app.projects.$projectId.tsx +++ b/frontend/src/routes/_app.projects.$projectId.tsx @@ -7,10 +7,6 @@ import { useState } from "react"; import { cn } from "@/lib/utils"; import { builders, activity, currentUser } from "@/mocks/seed"; import { Markdown } from "@/components/shared/Markdown"; -import { ArrowLeft, Star, GitFork, Users2, Github } from "lucide-react"; -import { useState } from "react"; -import { cn } from "@/lib/utils"; -import { builders, activity } from "@/mocks/seed"; import { BackButton } from "@/components/shared/BackButton"; export const Route = createFileRoute("/_app/projects/$projectId")({ @@ -97,16 +93,6 @@ function ProjectDetail() { {p.members}
-
- - {p.stars} - - - {p.forks} - - - {p.members} -