feat: restore landing style and add separate courses/admin UX

This commit is contained in:
root
2026-02-06 16:10:41 +00:00
parent 3d488f22b7
commit 4ca66ea896
26 changed files with 2119 additions and 1054 deletions

View File

@ -1,4 +1,4 @@
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import { Controller, Get, Post, Param, Body, Query, Delete, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ModerationService } from './moderation.service';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
@ -15,6 +15,15 @@ export class ModerationController {
return this.moderationService.getPendingCourses(user.id);
}
@Get('courses')
async getCourses(
@CurrentUser() user: User,
@Query('status') status?: string,
@Query('search') search?: string,
): Promise<any> {
return this.moderationService.getCourses(user.id, { status, search });
}
@Post(':courseId/approve')
async approveCourse(@Param('courseId') courseId: string, @Body('note') note: string, @CurrentUser() user: User): Promise<any> {
return this.moderationService.approveCourse(user.id, courseId, note);
@ -34,4 +43,10 @@ export class ModerationController {
async unhideReview(@Param('reviewId') reviewId: string, @CurrentUser() user: User): Promise<any> {
return this.moderationService.unhideReview(user.id, reviewId);
}
@Delete(':courseId')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteCourse(@Param('courseId') courseId: string, @CurrentUser() user: User): Promise<void> {
await this.moderationService.deleteCourse(user.id, courseId);
}
}

View File

@ -1,4 +1,4 @@
import { Injectable, ForbiddenException } from '@nestjs/common';
import { Injectable, ForbiddenException, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../common/prisma/prisma.service';
import { CourseStatus, UserRole } from '@coursecraft/database';
@ -22,6 +22,48 @@ export class ModerationService {
});
}
async getCourses(
userId: string,
options?: {
status?: string;
search?: string;
}
): Promise<any[]> {
await this.assertStaff(userId);
const allowedStatuses = Object.values(CourseStatus);
const where: any = {};
if (options?.status && allowedStatuses.includes(options.status as CourseStatus)) {
where.status = options.status as CourseStatus;
}
if (options?.search?.trim()) {
where.OR = [
{ title: { contains: options.search.trim(), mode: 'insensitive' } },
{ description: { contains: options.search.trim(), mode: 'insensitive' } },
{ author: { name: { contains: options.search.trim(), mode: 'insensitive' } } },
{ author: { email: { contains: options.search.trim(), mode: 'insensitive' } } },
];
}
return this.prisma.course.findMany({
where,
include: {
author: { select: { id: true, name: true, email: true } },
_count: {
select: {
chapters: true,
enrollments: true,
reviews: true,
},
},
},
orderBy: [{ status: 'asc' }, { updatedAt: 'desc' }],
take: 200,
});
}
async approveCourse(userId: string, courseId: string, note?: string): Promise<any> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user || (user.role !== UserRole.MODERATOR && user.role !== UserRole.ADMIN)) {
@ -98,6 +140,19 @@ export class ModerationService {
return review;
}
async deleteCourse(userId: string, courseId: string): Promise<void> {
await this.assertStaff(userId);
const existing = await this.prisma.course.findUnique({
where: { id: courseId },
select: { id: true },
});
if (!existing) {
throw new NotFoundException('Course not found');
}
await this.prisma.course.delete({ where: { id: courseId } });
}
private async assertStaff(userId: string): Promise<void> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user || (user.role !== UserRole.MODERATOR && user.role !== UserRole.ADMIN)) {