File size: 8,628 Bytes
41906d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32b4d43
41906d6
 
 
32b4d43
 
41906d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32b4d43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41906d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import {
  Controller,
  Get,
  Post,
  Put,
  Patch,
  Delete,
  Body,
  Param,
  Query,
  Req,
  UseGuards,
  ParseIntPipe,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import {
  ApiTags,
  ApiOperation,
  ApiParam,
  ApiBody,
  ApiResponse,
  ApiBearerAuth,
  ApiQuery,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/roles.decorator';
import { RoleName } from '../../auth/entities/role.entity';
import { CommunityPostsService } from '../services/community-posts.service';
import {
  CreatePostDto,
  UpdatePostDto,
  PostQueryDto,
  CreateCommentDto,
  CreateReactionDto,
} from '../dto';

@ApiTags('🌐 Community Posts')
@ApiBearerAuth('JWT-auth')
@Controller('api/community/posts')
@UseGuards(JwtAuthGuard, RolesGuard)
export class CommunityPostsController {
  constructor(private readonly postsService: CommunityPostsService) {}

  @Get()
  @ApiOperation({
    summary: 'List community posts',
    description: `
List community posts with optional filtering by course, category, and post type.

**Sorting Options**:
- \`recent\`: Most recent posts first (default)
- \`popular\`: Most upvoted posts first
- \`most_comments\`: Posts with most comments first

**Note**: Pinned posts always appear at the top regardless of sort order.
    `,
  })
  @ApiResponse({ status: 200, description: 'Paginated list of posts' })
  async findAll(@Query() query: PostQueryDto) {
    return this.postsService.findAll(query);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({
    summary: 'Create a new post',
    description: `
Create a new community post in a course.

**Post Types**:
- \`discussion\`: General discussion topic
- \`question\`: Question seeking answers
- \`announcement\`: Important announcement (different from admin announcements)
- \`resource\`: Shared resource or material
    `,
  })
  @ApiBody({ type: CreatePostDto })
  @ApiResponse({ status: 201, description: 'Post created successfully' })
  async create(@Body() dto: CreatePostDto, @Req() req: any) {
    const userId = req.user.userId || req.user.id;
    return this.postsService.create(dto, userId);
  }

  @Get(':id')
  @ApiOperation({
    summary: 'Get post with comments',
    description: 'Get a single post with paginated comments and reaction summary. Increments view count.',
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiQuery({ name: 'page', required: false, description: 'Comment page number', example: 1 })
  @ApiQuery({ name: 'limit', required: false, description: 'Comments per page', example: 50 })
  @ApiResponse({ status: 200, description: 'Post with comments and reactions' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async findOne(
    @Param('id', ParseIntPipe) id: number,
    @Req() req: any,
    @Query('page') page?: number,
    @Query('limit') limit?: number,
  ) {
    const userId = req.user?.userId || req.user?.id;
    return this.postsService.findOne(id, userId, page || 1, limit || 50);
  }

  @Put(':id')
  @ApiOperation({
    summary: 'Update post',
    description: `
Update a community post. 

**Note**: Only the post owner can update their posts.
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiBody({ type: UpdatePostDto })
  @ApiResponse({ status: 200, description: 'Post updated successfully' })
  @ApiResponse({ status: 403, description: 'Forbidden - Not the owner' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdatePostDto,
    @Req() req: any,
  ) {
    const userId = req.user.userId || req.user.id;
    const roles = this.extractRoles(req.user);
    return this.postsService.update(id, dto, userId, roles);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  @ApiOperation({
    summary: 'Delete post',
    description: `
Delete a community post.

**Allowed**: Post owner or Admin
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiResponse({ status: 204, description: 'Post deleted successfully' })
  @ApiResponse({ status: 403, description: 'Forbidden - Not owner or admin' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async remove(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
    const userId = req.user.userId || req.user.id;
    const roles = this.extractRoles(req.user);
    return this.postsService.remove(id, userId, roles);
  }

  @Post(':id/comment')
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({
    summary: 'Add comment to post',
    description: `
Add a comment to a community post. Supports nested replies via \`parentCommentId\`.

**Note**: Cannot comment on locked posts.
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiBody({ type: CreateCommentDto })
  @ApiResponse({ status: 201, description: 'Comment added successfully' })
  @ApiResponse({ status: 400, description: 'Post is locked' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async addComment(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: CreateCommentDto,
    @Req() req: any,
  ) {
    const userId = req.user.userId || req.user.id;
    return this.postsService.addComment(id, dto, userId);
  }

  @Post(':id/react')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Toggle reaction on post',
    description: `
Add or remove a reaction on a post. If the same reaction type exists, it will be removed (toggle behavior).

**Reaction Types**: like, helpful, insightful, thanks
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiBody({ type: CreateReactionDto })
  @ApiResponse({ status: 200, description: 'Reaction toggled. Returns action (added/removed)' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async toggleReaction(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: CreateReactionDto,
    @Req() req: any,
  ) {
    const userId = req.user.userId || req.user.id;
    return this.postsService.toggleReaction(id, dto, userId);
  }

  @Post(':id/upvote')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Toggle upvote on post',
    description: 'Add or remove an upvote on a post.',
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiResponse({ status: 200, description: 'Upvote toggled. Returns action (added/removed)' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async toggleUpvote(
    @Param('id', ParseIntPipe) id: number,
    @Req() req: any,
  ) {
    const userId = req.user.userId || req.user.id;
    return this.postsService.toggleReaction(id, { reactionType: 'upvote' as any }, userId);
  }

  @Patch(':id/pin')
  @Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
  @ApiOperation({
    summary: 'Toggle pin post',
    description: `
Pin or unpin a community post. Pinned posts appear at the top of listings.

**Allowed Roles**: Instructor, Admin
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiResponse({ status: 200, description: 'Pin status toggled' })
  @ApiResponse({ status: 403, description: 'Forbidden - Insufficient permissions' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async togglePin(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
    const userId = req.user.userId || req.user.id;
    const roles = this.extractRoles(req.user);
    return this.postsService.togglePin(id, userId, roles);
  }

  @Patch(':id/lock')
  @Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
  @ApiOperation({
    summary: 'Toggle lock post',
    description: `
Lock or unlock a community post. Locked posts cannot receive new comments.

**Allowed Roles**: Instructor, Admin
    `,
  })
  @ApiParam({ name: 'id', description: 'Post ID', example: 1 })
  @ApiResponse({ status: 200, description: 'Lock status toggled' })
  @ApiResponse({ status: 403, description: 'Forbidden - Insufficient permissions' })
  @ApiResponse({ status: 404, description: 'Post not found' })
  async toggleLock(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
    const userId = req.user.userId || req.user.id;
    const roles = this.extractRoles(req.user);
    return this.postsService.toggleLock(id, userId, roles);
  }

  /**
   * Helper to extract role names from user object
   */
  private extractRoles(user: any): string[] {
    if (!user.roles) return [];
    return user.roles.map((r: any) => r.roleName || r.name || r);
  }
}