Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions backend/migrations/015_study_groups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- Study groups table
CREATE TABLE IF NOT EXISTS study_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT,
invite_code VARCHAR(6) NOT NULL UNIQUE,
is_private BOOLEAN DEFAULT FALSE,
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Study group members table
CREATE TABLE IF NOT EXISTS study_group_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(10) NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (group_id, user_id)
);

-- Group study sets (shared study sets within a group)
CREATE TABLE IF NOT EXISTS group_study_sets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
study_set_id UUID NOT NULL,
shared_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (group_id, study_set_id)
);

-- Group activity feed
CREATE TABLE IF NOT EXISTS group_activity (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
action VARCHAR(50) NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_study_groups_invite_code ON study_groups(invite_code);
CREATE INDEX IF NOT EXISTS idx_study_groups_created_by ON study_groups(created_by);
CREATE INDEX IF NOT EXISTS idx_study_group_members_group_id ON study_group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_study_group_members_user_id ON study_group_members(user_id);
CREATE INDEX IF NOT EXISTS idx_group_study_sets_group_id ON group_study_sets(group_id);
CREATE INDEX IF NOT EXISTS idx_group_activity_group_id ON group_activity(group_id);
CREATE INDEX IF NOT EXISTS idx_group_activity_created_at ON group_activity(created_at DESC);
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { SubscriptionModule } from './modules/subscription';
import { AnalyticsModule } from './modules/analytics';
import { NotificationsModule } from './modules/notifications';
import { BlogModule } from './modules/blog';
import { StudyGroupsModule } from './modules/study-groups';

// Common
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
Expand Down Expand Up @@ -97,6 +98,7 @@ import { HealthController } from './health.controller';
AnalyticsModule,
NotificationsModule,
BlogModule,
StudyGroupsModule,
],
controllers: [HealthController],
providers: [
Expand Down
3 changes: 3 additions & 0 deletions backend/src/modules/study-groups/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './study-groups.module';
export * from './study-groups.service';
export * from './study-groups.controller';
120 changes: 120 additions & 0 deletions backend/src/modules/study-groups/study-groups.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
UseGuards,
ParseUUIDPipe,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CurrentUser, JwtPayload } from '../../common';
import {
StudyGroupsService,
CreateStudyGroupDto,
ShareStudySetDto,
} from './study-groups.service';

@ApiTags('Study Groups')
@Controller('study-groups')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class StudyGroupsController {
constructor(private readonly studyGroupsService: StudyGroupsService) {}

@Post()
@ApiOperation({ summary: 'Create a new study group' })
@ApiResponse({ status: 201, description: 'Study group created' })
async createGroup(
@CurrentUser() user: JwtPayload,
@Body() dto: CreateStudyGroupDto,
) {
return this.studyGroupsService.createGroup(user.sub, dto);
}

@Get()
@ApiOperation({ summary: 'List my study groups' })
@ApiResponse({ status: 200, description: 'List of study groups' })
async listGroups(@CurrentUser() user: JwtPayload) {
return this.studyGroupsService.listGroups(user.sub);
}

@Post('join')
@ApiOperation({ summary: 'Join a study group via invite code' })
@ApiResponse({ status: 200, description: 'Joined study group' })
async joinGroup(
@CurrentUser() user: JwtPayload,
@Body('inviteCode') inviteCode: string,
) {
return this.studyGroupsService.joinGroup(user.sub, inviteCode);
}

@Get(':id')
@ApiOperation({ summary: 'Get study group details' })
@ApiResponse({ status: 200, description: 'Study group details' })
async getGroupDetails(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.studyGroupsService.getGroupDetails(user.sub, id);
}

@Get(':id/members')
@ApiOperation({ summary: 'Get study group members' })
@ApiResponse({ status: 200, description: 'List of group members' })
async getGroupMembers(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.studyGroupsService.getGroupMembers(user.sub, id);
}

@Post(':id/share')
@ApiOperation({ summary: 'Share a study set with the group' })
@ApiResponse({ status: 201, description: 'Study set shared' })
async shareStudySet(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: ShareStudySetDto,
) {
return this.studyGroupsService.shareStudySet(user.sub, id, dto);
}

@Get(':id/study-sets')
@ApiOperation({ summary: 'Get study sets shared with the group' })
@ApiResponse({ status: 200, description: 'List of shared study sets' })
async getGroupStudySets(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.studyGroupsService.getGroupStudySets(user.sub, id);
}

@Get(':id/activity')
@ApiOperation({ summary: 'Get group activity feed' })
@ApiResponse({ status: 200, description: 'Activity feed' })
async getGroupActivity(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.studyGroupsService.getGroupActivity(user.sub, id);
}

@Delete(':id/leave')
@ApiOperation({ summary: 'Leave a study group' })
@ApiResponse({ status: 200, description: 'Left the group' })
async leaveGroup(
@CurrentUser() user: JwtPayload,
@Param('id', ParseUUIDPipe) id: string,
) {
await this.studyGroupsService.leaveGroup(user.sub, id);
return { message: 'Successfully left the group' };
}
}
10 changes: 10 additions & 0 deletions backend/src/modules/study-groups/study-groups.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { StudyGroupsService } from './study-groups.service';
import { StudyGroupsController } from './study-groups.controller';

@Module({
controllers: [StudyGroupsController],
providers: [StudyGroupsService],
exports: [StudyGroupsService],
})
export class StudyGroupsModule {}
Loading
Loading