feat: implement group creation and management (US #28)

Backend:
- Add groups Django app with models: Group, GroupMember, GroupInvite, JoinRequest
- Create serializers with business rule validation
- Implement GroupViewSet with full CRUD + custom actions (members, invites, roles, leave, join requests)
- Add JoinGroupViewSet for invite-based joining flow
- Register app in Django config and URL routing

Frontend:
- Add shared types for groups to @cloud-reader/shared
- Create groups API client (groupsApi)
- Build GroupsListPage, GroupDetailPage (member mgmt, invites, role transfer)
- Build CreateGroupPage and JoinGroupPage
- Add lazy-loaded routes to App.tsx with ProtectedRoute
- Add navigation links to Library header

Ref: #28
This commit is contained in:
Marko
2026-06-20 19:25:46 +00:00
parent 26c5f6f06b
commit d5def297b9
21 changed files with 1813 additions and 0 deletions
+96
View File
@@ -215,4 +215,100 @@ export interface PaginatedResponse<T> {
export interface ApiError {
detail?: string;
[key: string]: unknown;
}
// ---- Groups ----
export type GroupRole = "admin" | "member";
export type JoinRequestStatus = "pending" | "approved" | "rejected";
export interface GroupListItem {
id: number;
name: string;
description: string;
created_by: number;
member_count: number;
user_role: GroupRole | null;
created_at: string;
updated_at: string;
}
export interface GroupMember {
id: number;
user_id: number;
user_email: string;
user_username: string;
role: GroupRole;
joined_at: string;
}
export interface GroupDetail {
id: number;
name: string;
description: string;
created_by: number;
created_by_email: string;
created_by_username: string;
members: GroupMember[];
member_count: number;
user_role: GroupRole | null;
created_at: string;
updated_at: string;
}
export interface CreateGroupPayload {
name: string;
description?: string;
}
export interface UpdateGroupPayload {
name?: string;
description?: string;
}
export interface GroupInvite {
id: number;
group: number;
group_name: string;
code: string;
created_by: number;
created_by_email: string;
max_uses: number;
use_count: number;
is_active: boolean;
join_url: string;
created_at: string;
}
export interface CreateInvitePayload {
max_uses?: number;
}
export interface JoinRequest {
id: number;
group: number;
group_name: string;
user: number;
user_id: number;
user_email: string;
user_username: string;
invite: number | null;
invite_code: string | null;
status: JoinRequestStatus;
created_at: string;
}
export interface InviteValidation {
group: {
id: number;
name: string;
description: string;
created_by_email: string;
member_count: number;
};
invite: {
code: string;
created_by_email: string;
};
}
+14
View File
@@ -114,4 +114,18 @@ export const API_ENDPOINTS = {
bookmarkDetail: (id: string) => `/api/annotations/bookmarks/${id}/`,
noteDetail: (id: string) => `/api/annotations/notes/${id}/`,
},
groups: {
list: "/api/groups/",
detail: (id: number) => `/api/groups/${id}/`,
members: (id: number) => `/api/groups/${id}/members/`,
removeMember: (id: number, userId: number) => `/api/groups/${id}/members/${userId}/`,
updateRole: (id: number, userId: number) => `/api/groups/${id}/members/${userId}/role/`,
leave: (id: number) => `/api/groups/${id}/leave/`,
invites: (id: number) => `/api/groups/${id}/invites/`,
revokeInvite: (id: number, inviteId: number) => `/api/groups/${id}/invites/${inviteId}/`,
requests: (id: number) => `/api/groups/${id}/requests/`,
approveRequest: (id: number, requestId: number) => `/api/groups/${id}/requests/${requestId}/approve/`,
rejectRequest: (id: number, requestId: number) => `/api/groups/${id}/requests/${requestId}/reject/`,
join: (code: string) => `/api/join/${code}/`,
},
} as const;