diff --git a/.gitignore b/.gitignore index 293d4f1..7d4585e 100755 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ yarn-error.log* # Ignore .idea files in the Expo monorepo **/.idea/ + +uploads \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 74c2da1..53cb919 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,13 @@ # 基础镜像 FROM node:20-alpine as base # 更改 apk 镜像源为阿里云 -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ - yarn config set registry https://registry.npmmirror.com && \ - yarn global add pnpm && \ - pnpm config set registry https://registry.npmmirror.com +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +# 设置 npm 镜像源 +RUN yarn config set registry https://registry.npmmirror.com + +# 全局安装 pnpm 并设置其镜像源 +RUN yarn global add pnpm && pnpm config set registry https://registry.npmmirror.com + # 设置工作目录 WORKDIR /app @@ -14,27 +17,25 @@ COPY pnpm-workspace.yaml ./ # 首先复制 package.json, package-lock.json 和 pnpm-lock.yaml 文件 COPY package*.json pnpm-lock.yaml* ./ -COPY tsconfig.base.json . - +COPY tsconfig.json . +# 利用 Docker 缓存机制,如果依赖没有改变则不会重新执行 pnpm install +#100-500 5-40 FROM base As server-build WORKDIR /app COPY packages/common /app/packages/common COPY apps/server /app/apps/server -RUN pnpm install --filter common && \ - pnpm install --filter server && \ - pnpm --filter common generate && \ - pnpm --filter common build:cjs && \ - pnpm --filter server build +RUN pnpm install --filter server +RUN pnpm install --filter common +RUN pnpm --filter common generate && pnpm --filter common build:cjs +RUN pnpm --filter server build FROM base As server-prod-dep WORKDIR /app COPY packages/common /app/packages/common COPY apps/server /app/apps/server -RUN pnpm install --filter common --prod && \ - pnpm install --filter server --prod && \ - # 清理包管理器缓存 - pnpm store prune && rm -rf /root/.npm && rm -rf /root/.cache +RUN pnpm install --filter common --prod +RUN pnpm install --filter server --prod @@ -44,8 +45,9 @@ ENV NODE_ENV production COPY --from=server-build /app/packages/common/dist ./packages/common/dist COPY --from=server-build /app/apps/server/dist ./apps/server/dist COPY apps/server/entrypoint.sh ./apps/server/entrypoint.sh + RUN chmod +x ./apps/server/entrypoint.sh -# RUN apk add --no-cache postgresql-client +RUN apk add --no-cache postgresql-client EXPOSE 3000 @@ -57,7 +59,9 @@ ENTRYPOINT [ "/app/apps/server/entrypoint.sh" ] FROM base AS web-build # 复制其余文件到工作目录 COPY . . -RUN pnpm install && pnpm --filter web build +RUN pnpm install +RUN pnpm --filter web build + # 第二阶段,使用 nginx 提供服务 FROM nginx:stable-alpine as web RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories @@ -74,10 +78,6 @@ COPY apps/web/nginx.conf /etc/nginx/conf.d COPY apps/web/entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh # 安装 envsubst 以支持环境变量替换 -# RUN apk add --no-cache envsubst -# RUN echo "http://mirrors.aliyun.com/alpine/v3.12/main/" > /etc/apk/repositories && \ -# echo "http://mirrors.aliyun.com/alpine/v3.12/community/" >> /etc/apk/repositories && \ -# apk update && \ RUN apk add --no-cache gettext # 暴露 80 端口 EXPOSE 80 @@ -85,37 +85,20 @@ EXPOSE 80 CMD ["/usr/bin/entrypoint.sh"] -FROM python:3.10-slim as aiservice +FROM nginx:stable-alpine as nginx +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories # 设置工作目录 -WORKDIR /app +WORKDIR /usr/share/nginx/html -# 将 pip.conf 文件复制到镜像中 -# COPY apps/ai-service/config/pip.conf /etc/pip.conf - -# 将 requirements.txt 复制到工作目录 -COPY apps/ai-service/requirements.txt . -# 设置 pip 使用国内源,并安装依赖 -RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && pip install --no-cache-dir -r requirements.txt -# 暴露端口 -EXPOSE 8000 - -# 使用 entrypoint.sh 作为入口点 -ENTRYPOINT ["/app/entrypoint.sh"] - -FROM base As prisma-build -WORKDIR /app -COPY packages/common /app/packages/common -RUN pnpm install --filter common -RUN pnpm --filter common build - -FROM base As prisma-prod-dep -WORKDIR /app -COPY packages/common /app/packages/common -RUN pnpm install --filter common --prod - -FROM prisma-prod-dep as prisma-client -WORKDIR /app +# 设置环境变量 ENV NODE_ENV production -COPY --from=prisma-build /app/packages/common/dist ./packages/common/dist -CMD ["pnpm", "prisma", "migrate", "deploy"] \ No newline at end of file + +# 安装 envsubst 以支持环境变量替换 +RUN apk add --no-cache gettext + +# 暴露 80 端口 +EXPOSE 80 + +# 可选:复制自定义的 nginx 配置 +# COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/apps/server/.env.example b/apps/server/.env.example index d3c6f3b..30fc108 100755 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -11,3 +11,4 @@ DEADLINE_CRON="0 0 8 * * *" SERVER_PORT=3000 ADMIN_PHONE_NUMBER=13258117304 NODE_ENV=development +UPLOAD_DIR=/opt/projects/remooc/uploads \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 6ad92f7..b706afa 100755 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -31,6 +31,9 @@ "@nestjs/websockets": "^10.3.10", "@nicestack/common": "workspace:*", "@trpc/server": "11.0.0-rc.456", + "@tus/file-store": "^1.5.1", + "@tus/s3-store": "^1.6.2", + "@tus/server": "^1.10.0", "argon2": "^0.41.1", "axios": "^1.7.2", "bullmq": "^5.12.0", @@ -38,16 +41,20 @@ "dayjs": "^1.11.13", "dotenv": "^16.4.7", "exceljs": "^4.4.0", + "fluent-ffmpeg": "^2.1.3", "ioredis": "^5.4.1", "lib0": "^0.2.97", "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", + "mime-types": "^2.1.35", "minio": "^8.0.1", "mitt": "^3.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "sharp": "^0.33.5", "socket.io": "^4.7.5", "superjson-cjs": "^2.2.3", + "transliteration": "^2.3.5", "tus-js-client": "^4.1.0", "uuid": "^10.0.0", "ws": "^8.18.0", @@ -61,7 +68,9 @@ "@nestjs/testing": "^10.0.0", "@types/exceljs": "^1.3.0", "@types/express": "^4.17.21", + "@types/fluent-ffmpeg": "^2.1.27", "@types/jest": "^29.5.2", + "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index e126092..3bc892f 100755 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -17,6 +17,7 @@ import { CollaborationModule } from './socket/collaboration/collaboration.module import { ExceptionsFilter } from './filters/exceptions.filter'; import { TransformModule } from './models/transform/transform.module'; import { RealTimeModule } from './socket/realtime/realtime.module'; +import { UploadModule } from './upload/upload.module'; @Module({ imports: [ @@ -40,7 +41,8 @@ import { RealTimeModule } from './socket/realtime/realtime.module'; TransformModule, MinioModule, CollaborationModule, - RealTimeModule + RealTimeModule, + UploadModule ], providers: [{ provide: APP_FILTER, diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts index 3f7f4c4..d82ab36 100755 --- a/apps/server/src/auth/auth.controller.ts +++ b/apps/server/src/auth/auth.controller.ts @@ -1,13 +1,59 @@ -import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common'; +import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthSchema, JwtPayload } from '@nicestack/common'; import { AuthGuard } from './auth.guard'; import { UserProfileService } from './utils'; import { z } from 'zod'; +import { FileValidationErrorType } from './types'; @Controller('auth') export class AuthController { + private logger = new Logger(AuthController.name) constructor(private readonly authService: AuthService) { } - @UseGuards(AuthGuard) + @Get('file') + async authFileRequset( + @Headers('x-original-uri') originalUri: string, + @Headers('x-real-ip') realIp: string, + @Headers('x-original-method') method: string, + @Headers('x-query-params') queryParams: string, + @Headers('host') host: string, + @Headers('authorization') authorization: string, + ) { + + try { + const fileRequest = { + originalUri, + realIp, + method, + queryParams, + host, + authorization + }; + const authResult = await this.authService.validateFileRequest(fileRequest); + if (!authResult.isValid) { + // 使用枚举类型进行错误处理 + switch (authResult.error) { + case FileValidationErrorType.INVALID_URI: + throw new BadRequestException(authResult.error); + case FileValidationErrorType.RESOURCE_NOT_FOUND: + throw new NotFoundException(authResult.error); + case FileValidationErrorType.AUTHORIZATION_REQUIRED: + case FileValidationErrorType.INVALID_TOKEN: + throw new UnauthorizedException(authResult.error); + default: + throw new InternalServerErrorException(authResult.error || FileValidationErrorType.UNKNOWN_ERROR); + } + } + return { + headers: { + 'X-User-Id': authResult.userId, + 'X-Resource-Type': authResult.resourceType, + }, + }; + } catch (error: any) { + this.logger.verbose(`File request auth failed from ${realIp} reason:${error.message}`) + throw error; + } + } @Get('user-profile') async getUserProfile(@Req() request: Request) { const payload: JwtPayload = (request as any).user; diff --git a/apps/server/src/auth/auth.guard.ts b/apps/server/src/auth/auth.guard.ts index d0cd66d..ab4cac0 100644 --- a/apps/server/src/auth/auth.guard.ts +++ b/apps/server/src/auth/auth.guard.ts @@ -6,15 +6,16 @@ import { } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { env } from '@server/env'; -import { Request } from 'express'; + import { JwtPayload } from '@nicestack/common'; +import { extractTokenFromHeader } from './utils'; @Injectable() export class AuthGuard implements CanActivate { constructor(private jwtService: JwtService) { } async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const token = this.extractTokenFromHeader(request); + const token = extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); @@ -36,8 +37,5 @@ export class AuthGuard implements CanActivate { return true; } - extractTokenFromHeader(request: Request): string | undefined { - const [type, token] = request.headers.authorization?.split(' ') ?? []; - return type === 'Bearer' ? token : undefined; - } + } \ No newline at end of file diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts index 5abc663..7d119d7 100755 --- a/apps/server/src/auth/auth.module.ts +++ b/apps/server/src/auth/auth.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { StaffModule } from '@server/models/staff/staff.module'; -import { AuthRouter } from './auth.router'; import { TrpcService } from '@server/trpc/trpc.service'; import { DepartmentService } from '@server/models/department/department.service'; import { SessionService } from './session.service'; @@ -10,8 +9,8 @@ import { RoleMapModule } from '@server/models/rbac/rbac.module'; @Module({ imports: [StaffModule, RoleMapModule], - providers: [AuthService, AuthRouter, TrpcService, DepartmentService, SessionService], - exports: [AuthRouter, AuthService], + providers: [AuthService, TrpcService, DepartmentService, SessionService], + exports: [AuthService], controllers: [AuthController], }) export class AuthModule { } diff --git a/apps/server/src/auth/auth.router.ts b/apps/server/src/auth/auth.router.ts deleted file mode 100755 index 790cde9..0000000 --- a/apps/server/src/auth/auth.router.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TrpcService } from '@server/trpc/trpc.service'; -import { AuthSchema } from '@nicestack/common'; -import { AuthService } from './auth.service'; - -@Injectable() -export class AuthRouter { - constructor( - private readonly trpc: TrpcService, - private readonly authService: AuthService, - ) { } - router = this.trpc.router({ - signUp: this.trpc.procedure - .input(AuthSchema.signUpRequest) - .mutation(async ({ input }) => { - const result = await this.authService.signUp(input); - return result; - }), - }); -} diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index 5b93b06..e0b6c04 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -4,6 +4,7 @@ import { BadRequestException, Logger, InternalServerErrorException, + } from '@nestjs/common'; import { StaffService } from '../models/staff/staff.service'; import { @@ -14,11 +15,12 @@ import { import * as argon2 from 'argon2'; import { JwtService } from '@nestjs/jwt'; import { redis } from '@server/utils/redis/redis.service'; -import { UserProfileService } from './utils'; +import { extractTokenFromAuthorization, UserProfileService } from './utils'; import { SessionInfo, SessionService } from './session.service'; import { tokenConfig } from './config'; import { z } from 'zod'; - +import { FileAuthResult, FileRequest, FileValidationErrorType } from './types'; +import { extractFilePathFromUri } from '@server/utils/file'; @Injectable() export class AuthService { private logger = new Logger(AuthService.name) @@ -28,6 +30,46 @@ export class AuthService { private readonly sessionService: SessionService, ) { } + async validateFileRequest(params: FileRequest): Promise { + try { + // 基础参数验证 + if (!params?.originalUri) { + return { isValid: false, error: FileValidationErrorType.INVALID_URI }; + } + const fileId = extractFilePathFromUri(params.originalUri); + const resource = await db.resource.findFirst({ where: { fileId } }); + // 资源验证 + if (!resource) { + return { isValid: false, error: FileValidationErrorType.RESOURCE_NOT_FOUND }; + } + // 处理公开资源 + if (resource.isPublic) { + return { + isValid: true, + resourceType: resource.type || 'unknown' + }; + } + // 处理私有资源 + const token = extractTokenFromAuthorization(params.authorization); + if (!token) { + return { isValid: false, error: FileValidationErrorType.AUTHORIZATION_REQUIRED }; + } + const payload: JwtPayload = await this.jwtService.verify(token) + if (!payload.sub) { + return { isValid: false, error: FileValidationErrorType.INVALID_TOKEN }; + } + return { + isValid: true, + userId: payload.sub, + resourceType: resource.type || 'unknown' + }; + + } catch (error) { + this.logger.error('File validation error:', error); + return { isValid: false, error: FileValidationErrorType.UNKNOWN_ERROR }; + } + } + private async generateTokens(payload: JwtPayload): Promise<{ accessToken: string; refreshToken: string; diff --git a/apps/server/src/auth/types.ts b/apps/server/src/auth/types.ts index 3404d3c..4cb6c47 100644 --- a/apps/server/src/auth/types.ts +++ b/apps/server/src/auth/types.ts @@ -7,3 +7,25 @@ export interface TokenConfig { expirationMs: number; }; } + +export interface FileAuthResult { + isValid: boolean + userId?: string + resourceType?: string + error?: string +} +export interface FileRequest { + originalUri: string; + realIp: string; + method: string; + queryParams: string; + host: string; + authorization: string +} +export enum FileValidationErrorType { + INVALID_URI = 'INVALID_URI', + RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', + AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED', + INVALID_TOKEN = 'INVALID_TOKEN', + UNKNOWN_ERROR = 'UNKNOWN_ERROR' +} \ No newline at end of file diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index 234a3ae..1ebfb31 100644 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -11,7 +11,7 @@ import { env } from '@server/env'; import { redis } from '@server/utils/redis/redis.service'; import EventBus from '@server/utils/event-bus'; import { RoleMapService } from '@server/models/rbac/rolemap.service'; - +import { Request } from "express" interface ProfileResult { staff: UserProfile | undefined; error?: string; @@ -21,7 +21,14 @@ interface TokenVerifyResult { id?: string; error?: string; } - +export function extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = extractTokenFromAuthorization(request.headers.authorization) + return type === 'Bearer' ? token : undefined; +} +export function extractTokenFromAuthorization(authorization: string): string | undefined { + const [type, token] = authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; +} export class UserProfileService { public static readonly instance = new UserProfileService(); diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index ff75e3f..1875a5f 100755 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -16,7 +16,7 @@ async function bootstrap() { trpc.applyMiddleware(app); const port = process.env.SERVER_PORT || 3000; - + await app.listen(port); } diff --git a/apps/server/src/models/base/base.service.ts b/apps/server/src/models/base/base.service.ts index ba45d42..d6a1249 100644 --- a/apps/server/src/models/base/base.service.ts +++ b/apps/server/src/models/base/base.service.ts @@ -437,16 +437,25 @@ export class BaseService< pageSize?: number; where?: WhereArgs; select?: SelectArgs - }): Promise { + }): Promise<{ items: R['findMany']; totalPages: number }> { const { page = 1, pageSize = 10, where, select } = args; + try { - return this.getModel().findMany({ + // 获取总记录数 + const total = await this.getModel().count({ where }) as number; + // 获取分页数据 + const items = await this.getModel().findMany({ where, select, skip: (page - 1) * pageSize, take: pageSize, - - } as any) as Promise; + } as any) as R['findMany']; + // 计算总页数 + const totalPages = Math.ceil(total / pageSize); + return { + items, + totalPages + }; } catch (error) { this.handleError(error, 'read'); } diff --git a/apps/server/src/models/course/course.router.ts b/apps/server/src/models/course/course.router.ts index 758d8a8..f39e0cb 100644 --- a/apps/server/src/models/course/course.router.ts +++ b/apps/server/src/models/course/course.router.ts @@ -23,13 +23,13 @@ export class CourseRouter { .input(CourseCreateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; - return await this.courseService.create(input, staff); + return await this.courseService.create(input, { staff }); }), update: this.trpc.protectProcedure .input(CourseUpdateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; - return await this.courseService.update(input, staff); + return await this.courseService.update(input, { staff }); }), createMany: this.trpc.protectProcedure.input(z.array(CourseCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { @@ -74,7 +74,7 @@ export class CourseRouter { }), findManyWithPagination: this.trpc.procedure .input(z.object({ - page: z.number(), + page: z.number().optional(), pageSize: z.number().optional(), where: CourseWhereInputSchema.optional(), select: CourseSelectSchema.optional() diff --git a/apps/server/src/models/course/course.schema.ts b/apps/server/src/models/course/course.schema.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/server/src/models/course/course.service.ts b/apps/server/src/models/course/course.service.ts index 9374a7b..b5e534b 100644 --- a/apps/server/src/models/course/course.service.ts +++ b/apps/server/src/models/course/course.service.ts @@ -5,10 +5,74 @@ import { db, ObjectType, Prisma, + InstructorRole, } from '@nicestack/common'; @Injectable() export class CourseService extends BaseService { constructor() { super(db, ObjectType.COURSE); } + async create( + args: Prisma.CourseCreateArgs, + params?: { staff?: UserProfile } + ) { + return await db.$transaction(async tx => { + const result = await super.create(args, { tx }); + if (params?.staff?.id) { + await tx.courseInstructor.create({ + data: { + instructorId: params.staff.id, + courseId: result.id, + role: InstructorRole.MAIN, + } + }); + } + return result; + }, { + timeout: 10000 // 10 seconds + }); + } + async update( + args: Prisma.CourseUpdateArgs, + params?: { staff?: UserProfile } + ) { + return await db.$transaction(async tx => { + const result = await super.update(args, { tx }); + return result; + }, { + timeout: 10000 // 10 seconds + }); + } + async removeInstructor(courseId: string, instructorId: string) { + return await db.courseInstructor.delete({ + where: { + courseId_instructorId: { + courseId, + instructorId, + }, + }, + }); + } + async addInstructor(params: { + courseId: string; + instructorId: string; + role?: string; + order?: number; + }) { + return await db.courseInstructor.create({ + data: { + courseId: params.courseId, + instructorId: params.instructorId, + role: params.role || InstructorRole.ASSISTANT, + order: params.order, + }, + }); + } + async getInstructors(courseId: string) { + return await db.courseInstructor.findMany({ + where: { courseId }, + include: { instructor: true }, + orderBy: { order: 'asc' }, + }); + } } diff --git a/apps/server/src/models/course/utils.ts b/apps/server/src/models/course/utils.ts new file mode 100644 index 0000000..555cf85 --- /dev/null +++ b/apps/server/src/models/course/utils.ts @@ -0,0 +1,46 @@ +import { db, EnrollmentStatus, PostType } from "@nicestack/common"; + +// 更新课程评价统计 +export async function updateCourseReviewStats(courseId: string) { + const reviews = await db.post.findMany({ + where: { + courseId, + type: PostType.COURSE_REVIEW, + deletedAt: null + }, + select: { rating: true } + }); + const numberOfReviews = reviews.length; + const averageRating = numberOfReviews > 0 + ? reviews.reduce((sum, review) => sum + review.rating, 0) / numberOfReviews + : 0; + + return db.course.update({ + where: { id: courseId }, + data: { numberOfReviews, averageRating } + }); +} + +// 更新课程注册统计 +export async function updateCourseEnrollmentStats(courseId: string) { + const completedEnrollments = await db.enrollment.count({ + where: { + courseId, + status: EnrollmentStatus.COMPLETED + } + }); + const totalEnrollments = await db.enrollment.count({ + where: { courseId } + }); + const completionRate = totalEnrollments > 0 + ? (completedEnrollments / totalEnrollments) * 100 + : 0; + return db.course.update({ + where: { id: courseId }, + data: { + numberOfStudents: totalEnrollments, + completionRate + } + }); +} + diff --git a/apps/server/src/models/enrollment/enroll.schema.ts b/apps/server/src/models/enrollment/enroll.schema.ts new file mode 100644 index 0000000..41a9968 --- /dev/null +++ b/apps/server/src/models/enrollment/enroll.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const EnrollSchema = z.object({ + studentId: z.string(), + courseId: z.string(), +}); + +export const UnenrollSchema = z.object({ + studentId: z.string(), + courseId: z.string(), +}); \ No newline at end of file diff --git a/apps/server/src/models/enrollment/enrollment.router.ts b/apps/server/src/models/enrollment/enrollment.router.ts index 0b8ad1d..d11c095 100644 --- a/apps/server/src/models/enrollment/enrollment.router.ts +++ b/apps/server/src/models/enrollment/enrollment.router.ts @@ -3,6 +3,7 @@ import { TrpcService } from '@server/trpc/trpc.service'; import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { EnrollmentService } from './enrollment.service'; import { z, ZodType } from 'zod'; +import { EnrollSchema, UnenrollSchema } from './enroll.schema'; const EnrollmentCreateArgsSchema: ZodType = z.any() const EnrollmentCreateManyInputSchema: ZodType = z.any() const EnrollmentDeleteManyArgsSchema: ZodType = z.any() @@ -18,38 +19,11 @@ export class EnrollmentRouter { private readonly enrollmentService: EnrollmentService, ) { } router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(EnrollmentCreateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.enrollmentService.create(input, staff); - }), - createMany: this.trpc.protectProcedure.input(z.array(EnrollmentCreateManyInputSchema)) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - - return await this.enrollmentService.createMany({ data: input }, staff); - }), - deleteMany: this.trpc.procedure - .input(EnrollmentDeleteManyArgsSchema) - .mutation(async ({ input }) => { - return await this.enrollmentService.deleteMany(input); - }), findFirst: this.trpc.procedure .input(EnrollmentFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input }) => { return await this.enrollmentService.findFirst(input); }), - softDeleteByIds: this.trpc.protectProcedure - .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema - .mutation(async ({ input }) => { - return this.enrollmentService.softDeleteByIds(input.ids); - }), - updateOrder: this.trpc.protectProcedure - .input(UpdateOrderSchema) - .mutation(async ({ input }) => { - return this.enrollmentService.updateOrder(input); - }), findMany: this.trpc.procedure .input(EnrollmentFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input }) => { @@ -66,5 +40,15 @@ export class EnrollmentRouter { const { staff } = ctx; return await this.enrollmentService.findManyWithCursor(input); }), + enroll: this.trpc.protectProcedure + .input(EnrollSchema) + .mutation(async ({ input }) => { + return await this.enrollmentService.enroll(input); + }), + unenroll: this.trpc.protectProcedure + .input(UnenrollSchema) + .mutation(async ({ input }) => { + return await this.enrollmentService.unenroll(input); + }), }); } diff --git a/apps/server/src/models/enrollment/enrollment.service.ts b/apps/server/src/models/enrollment/enrollment.service.ts index 09e7326..3c5af86 100644 --- a/apps/server/src/models/enrollment/enrollment.service.ts +++ b/apps/server/src/models/enrollment/enrollment.service.ts @@ -1,17 +1,74 @@ -import { Injectable } from '@nestjs/common'; +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { BaseService } from '../base/base.service'; import { UserProfile, db, ObjectType, Prisma, - + EnrollmentStatus } from '@nicestack/common'; +import { z } from 'zod'; +import { EnrollSchema, UnenrollSchema } from './enroll.schema'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + @Injectable() export class EnrollmentService extends BaseService { constructor() { super(db, ObjectType.COURSE); } + async enroll(params: z.infer) { + const { studentId, courseId } = params + const result = await db.$transaction(async tx => { + // 检查是否已经报名 + const existing = await tx.enrollment.findUnique({ + where: { + studentId_courseId: { + studentId: studentId, + courseId: courseId, + }, + }, + }); + if (existing) { + throw new ConflictException('Already enrolled in this course'); + } + // 创建报名记录 + const enrollment = await tx.enrollment.create({ + data: { + studentId: studentId, + courseId: courseId, + status: EnrollmentStatus.ACTIVE, + }, + }); - + return enrollment; + }); + + EventBus.emit('dataChanged', { + type: ObjectType.ENROLLMENT, + operation: CrudOperation.CREATED, + data: result, + }); + return result + } + async unenroll(params: z.infer) { + const { studentId, courseId } = params + const result = await db.enrollment.update({ + where: { + studentId_courseId: { + studentId, + courseId, + }, + }, + data: { + status: EnrollmentStatus.CANCELLED + } + }); + + EventBus.emit('dataChanged', { + type: ObjectType.ENROLLMENT, + operation: CrudOperation.UPDATED, + data: result, + }); + return result + } } diff --git a/apps/server/src/models/lecture/lecture.router.ts b/apps/server/src/models/lecture/lecture.router.ts index f99b70c..affcdd4 100644 --- a/apps/server/src/models/lecture/lecture.router.ts +++ b/apps/server/src/models/lecture/lecture.router.ts @@ -22,7 +22,7 @@ export class LectureRouter { .input(LectureCreateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; - return await this.lectureService.create(input, staff); + return await this.lectureService.create(input, {staff}); }), createMany: this.trpc.protectProcedure.input(z.array(LectureCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { diff --git a/apps/server/src/models/lecture/lecture.service.ts b/apps/server/src/models/lecture/lecture.service.ts index 2f1efa4..66bbc12 100644 --- a/apps/server/src/models/lecture/lecture.service.ts +++ b/apps/server/src/models/lecture/lecture.service.ts @@ -4,14 +4,32 @@ import { UserProfile, db, ObjectType, - Prisma, - + Prisma } from '@nicestack/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + @Injectable() export class LectureService extends BaseService { constructor() { super(db, ObjectType.COURSE); } - + async create(args: Prisma.LectureCreateArgs, params?: { staff?: UserProfile }) { + const result = await super.create(args) + EventBus.emit('dataChanged', { + type: ObjectType.LECTURE, + operation: CrudOperation.CREATED, + data: result, + }); + return result; + } + async update(args: Prisma.LectureUpdateArgs) { + const result = await super.update(args); + EventBus.emit('dataChanged', { + type: ObjectType.LECTURE, + operation: CrudOperation.UPDATED, + data: result, + }); + return result; + } } diff --git a/apps/server/src/models/lecture/utils.ts b/apps/server/src/models/lecture/utils.ts new file mode 100644 index 0000000..a634d15 --- /dev/null +++ b/apps/server/src/models/lecture/utils.ts @@ -0,0 +1,39 @@ +import { db, Lecture } from "@nicestack/common" + +export async function updateSectionLectureStats(sectionId: string) { + const sectionStats = await db.lecture.aggregate({ + where: { + sectionId, + deletedAt: null + }, + _count: { _all: true }, + _sum: { duration: true } + }); + + await db.section.update({ + where: { id: sectionId }, + data: { + totalLectures: sectionStats._count._all, + totalDuration: sectionStats._sum.duration || 0 + } + }); +} + +export async function updateCourseLectureStats(courseId: string) { + const courseStats = await db.lecture.aggregate({ + where: { + courseId, + deletedAt: null + }, + _count: { _all: true }, + _sum: { duration: true } + }); + + await db.course.update({ + where: { id: courseId }, + data: { + totalLectures: courseStats._count._all, + totalDuration: courseStats._sum.duration || 0 + } + }); +} \ No newline at end of file diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 9abdf1a..f9f17ce 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -39,12 +39,17 @@ export class PostService extends BaseService { } async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) { args.data.authorId = staff?.id; - return super.update(args); + const result = await super.update(args); + EventBus.emit('dataChanged', { + type: ObjectType.POST, + operation: CrudOperation.UPDATED, + data: result, + }); + return result } async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) { if (!args.where) args.where = {} args.where.OR = await this.preFilter(args.where.OR, staff); - // console.log(`findwithcursor_post ${JSON.stringify(args.where)}`) return this.wrapResult(super.findManyWithCursor(args), async (result) => { let { items } = result; await Promise.all( diff --git a/apps/server/src/models/resource/pipe/resource.pipeline.ts b/apps/server/src/models/resource/pipe/resource.pipeline.ts new file mode 100644 index 0000000..9b0c962 --- /dev/null +++ b/apps/server/src/models/resource/pipe/resource.pipeline.ts @@ -0,0 +1,84 @@ +import { PrismaClient, Resource } from '@prisma/client' +import { ProcessResult, ResourceProcessor } from '../types' +import { db, ResourceProcessStatus } from '@nicestack/common' +import { Logger } from '@nestjs/common'; + + +// Pipeline 类 +export class ResourceProcessingPipeline { + private processors: ResourceProcessor[] = [] + private logger = new Logger(ResourceProcessingPipeline.name); + + constructor() { } + + // 添加处理器 + addProcessor(processor: ResourceProcessor): ResourceProcessingPipeline { + this.processors.push(processor) + return this + } + + // 执行处理管道 + async execute(resource: Resource): Promise { + let currentResource = resource + try { + this.logger.log(`开始处理资源: ${resource.id}`) + + currentResource = await this.updateProcessStatus( + resource.id, + ResourceProcessStatus.PROCESSING + ) + this.logger.log(`资源状态已更新为处理中`) + + for (const processor of this.processors) { + const processorName = processor.constructor.name + this.logger.log(`开始执行处理器: ${processorName}`) + + currentResource = await this.updateProcessStatus( + currentResource.id, + processor.constructor.name as ResourceProcessStatus + ) + + currentResource = await processor.process(currentResource) + this.logger.log(`处理器 ${processorName} 执行完成`) + + currentResource = await db.resource.update({ + where: { id: currentResource.id }, + data: currentResource + }) + } + + currentResource = await this.updateProcessStatus( + currentResource.id, + ResourceProcessStatus.SUCCESS + ) + this.logger.log(`资源 ${resource.id} 处理成功 ${JSON.stringify(currentResource.metadata)}`) + + return { + success: true, + resource: currentResource + } + } catch (error) { + this.logger.error(`资源 ${resource.id} 处理失败:`, error) + + currentResource = await this.updateProcessStatus( + currentResource.id, + ResourceProcessStatus.FAILED + ) + + return { + success: false, + resource: currentResource, + error: error as Error + } + } + } + private async updateProcessStatus( + resourceId: string, + processStatus: ResourceProcessStatus + ): Promise { + return db.resource.update({ + where: { id: resourceId }, + data: { processStatus } + }) + } +} diff --git a/apps/server/src/models/resource/processor/GeneralProcessor.ts b/apps/server/src/models/resource/processor/GeneralProcessor.ts new file mode 100644 index 0000000..652fddd --- /dev/null +++ b/apps/server/src/models/resource/processor/GeneralProcessor.ts @@ -0,0 +1,52 @@ +import { BaseMetadata, FileMetadata, ResourceProcessor } from "../types"; +import { Resource, db, ResourceProcessStatus } from "@nicestack/common"; +import { extname } from "path"; +import mime from "mime"; +import { calculateFileHash, getUploadFilePath } from "@server/utils/file"; +import { Logger } from "@nestjs/common"; +import { statSync } from "fs"; // Add this import +export class GeneralProcessor implements ResourceProcessor { + private logger = new Logger(GeneralProcessor.name); + + async process(resource: Resource): Promise { + const originMeta = resource.metadata as any; + try { + // 提取文件扩展名作为type + const fileExtension = extname(resource.filename).toLowerCase().slice(1); + this.logger.debug(`文件扩展名: ${fileExtension}`); + + // 获取文件路径并验证 + const filePath = getUploadFilePath(resource.fileId); + this.logger.debug(`文件路径: ${filePath}`); + const fileStats = statSync(filePath); + const fileSize = fileStats.size; + const modifiedAt = fileStats.mtime; + const createdAt = fileStats.birthtime; + + const fileHash = await calculateFileHash(filePath); + // 准备metadata + const metadata: BaseMetadata = { + filename: resource.filename, + extension: fileExtension, + mimeType: mime.lookup(fileExtension) || 'application/octet-stream', + size: fileSize, + modifiedAt, + createdAt, + }; + + const updatedResource = await db.resource.update({ + where: { id: resource.id }, + data: { + type: fileExtension, + hash: fileHash, + metadata: { ...originMeta, ...metadata } as any, + } + }); + + return updatedResource; + + } catch (error: any) { + throw new Error(`Resource processing failed: ${error.message}`); + } + } +} \ No newline at end of file diff --git a/apps/server/src/models/resource/processor/ImageProcessor.ts b/apps/server/src/models/resource/processor/ImageProcessor.ts new file mode 100644 index 0000000..cb42f50 --- /dev/null +++ b/apps/server/src/models/resource/processor/ImageProcessor.ts @@ -0,0 +1,65 @@ +import path from "path"; +import sharp from 'sharp'; +import { FileMetadata, ImageMetadata, ResourceProcessor } from "../types"; +import { Resource, ResourceProcessStatus, db } from "@nicestack/common"; +import { getUploadFilePath } from "@server/utils/file"; +import { Logger } from "@nestjs/common"; +import { promises as fsPromises } from 'fs'; +export class ImageProcessor implements ResourceProcessor { + private logger = new Logger(ImageProcessor.name) + constructor() { } + + async process(resource: Resource): Promise { + const { fileId } = resource; + const filepath = getUploadFilePath(fileId); + const originMeta = resource.metadata as unknown as FileMetadata; + + if (!originMeta.mimeType?.startsWith('image/')) { + this.logger.log(`Skipping non-image resource: ${resource.id}`); + return resource; + } + + try { + const image = sharp(filepath); + const metadata = await image.metadata(); + if (!metadata) { + throw new Error(`Failed to get metadata for image: ${fileId}`); + } + // Create WebP compressed version + const compressedPath = path.join( + path.dirname(filepath), + `${path.basename(filepath, path.extname(filepath))}_compressed.webp` + ); + await image + .webp({ + quality: 80, + lossless: false, + effort: 5 // Range 0-6, higher means slower but better compression + }) + .toFile(compressedPath); + const imageMeta: ImageMetadata = { + width: metadata.width || 0, + height: metadata.height || 0, + compressedUrl: path.basename(compressedPath), + orientation: metadata.orientation, + space: metadata.space, + hasAlpha: metadata.hasAlpha, + } + console.log(imageMeta) + const updatedResource = await db.resource.update({ + where: { id: resource.id }, + data: { + metadata: { + ...originMeta, + ...imageMeta + } + } + }); + + return updatedResource; + } catch (error: any) { + throw new Error(`Failed to process image: ${error.message}`); + } + } + +} \ No newline at end of file diff --git a/apps/server/src/models/resource/processor/VideoProcessor.ts b/apps/server/src/models/resource/processor/VideoProcessor.ts new file mode 100644 index 0000000..1d80eff --- /dev/null +++ b/apps/server/src/models/resource/processor/VideoProcessor.ts @@ -0,0 +1,9 @@ +// import ffmpeg from 'fluent-ffmpeg'; +// import { ResourceProcessor } from '../types'; +// import { Resource } from '@nicestack/common'; + +// export class VideoProcessor implements ResourceProcessor { +// async process(resource: Resource): Promise { + +// } +// } \ No newline at end of file diff --git a/apps/server/src/models/resource/resource.router.ts b/apps/server/src/models/resource/resource.router.ts index 59124d8..0751de3 100644 --- a/apps/server/src/models/resource/resource.router.ts +++ b/apps/server/src/models/resource/resource.router.ts @@ -22,7 +22,7 @@ export class ResourceRouter { .input(ResourceCreateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; - return await this.resourceService.create(input, staff); + return await this.resourceService.create(input, { staff }); }), createMany: this.trpc.protectProcedure.input(z.array(ResourceCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { diff --git a/apps/server/src/models/resource/resource.service.ts b/apps/server/src/models/resource/resource.service.ts index 4be9da4..4f71164 100644 --- a/apps/server/src/models/resource/resource.service.ts +++ b/apps/server/src/models/resource/resource.service.ts @@ -5,13 +5,34 @@ import { db, ObjectType, Prisma, - + Resource, } from '@nicestack/common'; +import { createHash } from 'crypto'; + @Injectable() export class ResourceService extends BaseService { constructor() { super(db, ObjectType.RESOURCE); } + async create( + args: Prisma.ResourceCreateArgs, + params?: { staff?: UserProfile }, + ): Promise { + if (params?.staff) { + args.data.ownerId = params?.staff?.id + } + return this.create(args); + } + async checkFileExists(hash: string): Promise { + return this.findFirst({ + where: { + hash, + deletedAt: null, + }, + }); + } + async calculateFileHash(buffer: Buffer): Promise { + return createHash('sha256').update(buffer).digest('hex'); + } - -} +} \ No newline at end of file diff --git a/apps/server/src/models/resource/types.ts b/apps/server/src/models/resource/types.ts new file mode 100644 index 0000000..842db41 --- /dev/null +++ b/apps/server/src/models/resource/types.ts @@ -0,0 +1,57 @@ +import { Resource } from "@nicestack/common"; + +export interface ResourceProcessor { + process(resource: Resource): Promise +}// 处理结果 +export interface ProcessResult { + success: boolean + resource: Resource + error?: Error +} + +export interface BaseMetadata { + size: number + mimeType: string + filename: string + extension: string + modifiedAt: Date + createdAt: Date +} +/** + * 图片特有元数据接口 + */ +export interface ImageMetadata { + width: number; // 图片宽度(px) + height: number; // 图片高度(px) + compressedUrl?:string; + orientation?: number; // EXIF方向信息 + space?: string; // 色彩空间 (如: RGB, CMYK) + hasAlpha?: boolean; // 是否包含透明通道 +} + +/** + * 视频特有元数据接口 + */ +export interface VideoMetadata { + width: number; // 视频宽度(px) + height: number; // 视频高度(px) + duration: number; // 视频时长(秒) + thumbnail?: string; // 视频封面图URL + codec?: string; // 视频编码格式 + frameRate?: number; // 帧率(fps) + bitrate?: number; // 比特率(bps) +} + +/** + * 音频特有元数据接口 + */ +export interface AudioMetadata { + duration: number; // 音频时长(秒) + bitrate?: number; // 比特率(bps) + sampleRate?: number; // 采样率(Hz) + channels?: number; // 声道数 + codec?: string; // 音频编码格式 +} + + +export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata \ No newline at end of file diff --git a/apps/server/src/models/section/section.router.ts b/apps/server/src/models/section/section.router.ts index bba664b..ef9ff3e 100644 --- a/apps/server/src/models/section/section.router.ts +++ b/apps/server/src/models/section/section.router.ts @@ -22,7 +22,7 @@ export class SectionRouter { .input(SectionCreateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; - return await this.sectionService.create(input, staff); + return await this.sectionService.create(input, { staff }); }), createMany: this.trpc.protectProcedure.input(z.array(SectionCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { diff --git a/apps/server/src/models/section/section.service.ts b/apps/server/src/models/section/section.service.ts index 15d4498..e926eff 100644 --- a/apps/server/src/models/section/section.service.ts +++ b/apps/server/src/models/section/section.service.ts @@ -13,5 +13,12 @@ export class SectionService extends BaseService { super(db, ObjectType.SECTION); } + create(args: Prisma.SectionCreateArgs, params?: { staff?: UserProfile }) { + return super.create(args) + } + async update(args: Prisma.SectionUpdateArgs) { + return super.update(args); + } + } diff --git a/apps/server/src/queue/job.interface.ts b/apps/server/src/queue/job.interface.ts deleted file mode 100755 index df8b45e..0000000 --- a/apps/server/src/queue/job.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CustomJobType = "pushMessage" | "updateTroubleViewCount" -export type updateViewCountJobData = { - id: string -} \ No newline at end of file diff --git a/apps/server/src/queue/push/push.queue.service.ts b/apps/server/src/queue/push/push.queue.service.ts deleted file mode 100755 index 87f1525..0000000 --- a/apps/server/src/queue/push/push.queue.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { InjectQueue } from "@nestjs/bullmq"; -import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; -import { Queue } from "bullmq"; -import { db, getUniqueItems, MessageDto, ObjectType } from "@nicestack/common" -import { MessageContent } from "./push.service"; -import EventBus, { CrudOperation } from "@server/utils/event-bus"; -export interface PushMessageJobData { - id: string - registerToken: string - messageContent: MessageContent -} -@Injectable() -export class PushQueueService implements OnModuleInit { - private readonly logger = new Logger(PushQueueService.name) - constructor(@InjectQueue('general') private generalQueue: Queue) { } - onModuleInit() { - EventBus.on("dataChanged", async ({ data, type, operation }) => { - if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) { - const message = data as MessageDto - const uniqueStaffs = getUniqueItems(message.receivers, "id") - - uniqueStaffs.forEach(item => { - const token = item.registerToken - if (token) { - this.addPushMessageJob({ - registerToken: token, - messageContent: { - data: { - title: message.title!, - content: message.content!, - click_action: { - intent: message.intent!, - url: message.url! - } - }, - option: message.option as any - }, - id: message.id - }) - } else { - this.logger.warn(`用户 ${item.username} 尚未注册registerToken取消消息推送`) - } - - }) - } - }) - } - async addPushMessageJob(data: PushMessageJobData) { - this.logger.log("add push message task", data.registerToken) - await this.generalQueue.add('pushMessage', data, { debounce: { id: data.id } }) - } - -} \ No newline at end of file diff --git a/apps/server/src/queue/push/push.service.ts b/apps/server/src/queue/push/push.service.ts deleted file mode 100755 index 8dfa56f..0000000 --- a/apps/server/src/queue/push/push.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable, InternalServerErrorException, BadRequestException, UnauthorizedException, NotFoundException } from '@nestjs/common'; -import axios, { AxiosResponse } from 'axios'; -interface LoginResponse { - retcode: string; - message: string; - authtoken?: string; -} -interface MessagePushResponse { - retcode: string; - message: string; - messageid?: string; -} -interface Notification { - title?: string; // 通知标题(不超过128字节) / Title of notification (upper limit is 128 bytes) - content?: string; // 通知内容(不超过256字节) / Content of notification (upper limit is 256 bytes) - click_action?: { - url?: string; // 点击通知栏消息,打开指定的URL地址 / URL to open when notification is clicked - intent?: string; // 点击通知栏消息,用户收到通知栏消息后点击通知栏消息打开应用定义的这个Intent页面 / Intent page to open in the app when notification is clicked - }; -} - -interface Option { - key: string; - value: string; -} - -export interface MessageContent { - data: Notification; - option?: Option; -} - -@Injectable() -export class PushService { - private readonly baseURL = process.env.PUSH_URL; - private readonly appid = process.env.PUSH_APPID; - private readonly appsecret = process.env.PUSH_APPSECRET; - private authToken: string | null = null; - async login(): Promise { - if (this.authToken) { - return { retcode: '200', message: 'Already logged in', authtoken: this.authToken }; - } - const url = `${this.baseURL}/push/1.0/login`; - const response: AxiosResponse = await axios.post(url, { - appid: this.appid, - appsecret: this.appsecret, - }); - this.handleError(response.data.retcode); - this.authToken = response.data.authtoken!; - return response.data; - } - async messagePush( - registerToken: string, - messageContent: MessageContent, - ): Promise { - if (!this.authToken) { - await this.login(); - } - const url = `${this.baseURL}/push/1.0/messagepush`; - const payload = { - appid: this.appid, - appsecret: this.appsecret, - authtoken: this.authToken, - registertoken: registerToken, - messagecontent: JSON.stringify(messageContent), - }; - const response: AxiosResponse = await axios.post(url, payload); - this.handleError(response.data.retcode); - return response.data; - } - - private handleError(retcode: string): void { - switch (retcode) { - case '000': - case '200': - return; - case '001': - throw new BadRequestException('JID is illegal'); - case '002': - throw new BadRequestException('AppID is illegal'); - case '003': - throw new BadRequestException('Protoversion is mismatch'); - case '010': - throw new BadRequestException('The application of AppID Token is repeated'); - case '011': - throw new BadRequestException('The number of applications for token exceeds the maximum'); - case '012': - throw new BadRequestException('Token is illegal'); - case '013': - throw new UnauthorizedException('Integrity check failed'); - case '014': - throw new BadRequestException('Parameter is illegal'); - case '015': - throw new InternalServerErrorException('Internal error'); - case '202': - throw new UnauthorizedException('You are already logged in'); - case '205': - return; - case '500': - throw new InternalServerErrorException('Internal Server Error'); - case '502': - throw new InternalServerErrorException('Session loading error'); - case '503': - throw new InternalServerErrorException('Service Unavailable'); - case '504': - throw new NotFoundException('Parameters not found'); - case '505': - throw new BadRequestException('Parameters are empty or not as expected'); - case '506': - throw new InternalServerErrorException('Database error'); - case '508': - throw new InternalServerErrorException('NoSuchAlgorithmException'); - case '509': - throw new UnauthorizedException('Authentication Failed'); - case '510': - throw new UnauthorizedException('Illegal token... client does not exist'); - default: - throw new InternalServerErrorException('Unexpected error occurred'); - } - } -} diff --git a/apps/server/src/queue/queue.module.ts b/apps/server/src/queue/queue.module.ts index 8729029..57a1a9d 100755 --- a/apps/server/src/queue/queue.module.ts +++ b/apps/server/src/queue/queue.module.ts @@ -2,8 +2,6 @@ import { BullModule } from '@nestjs/bullmq'; import { Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { join } from 'path'; -import { PushService } from './push/push.service'; -import { PushQueueService } from './push/push.queue.service'; @Module({ imports: [ @@ -19,13 +17,19 @@ import { PushQueueService } from './push/push.queue.service'; }), inject: [ConfigService], }), - BullModule.registerQueue({ - name: 'general', - processors: [join(__dirname, 'worker/processor.js')], - }) + BullModule.registerQueue( + { + name: 'general', + processors: [join(__dirname, 'worker/processor.js')], + }, + { + name: 'file-queue', // 新增文件处理队列 + processors: [join(__dirname, 'worker/file.processor.js')], // 文件处理器的路径 + } + ), ], - providers: [Logger, PushService, PushQueueService], - exports: [PushService, PushQueueService] + providers: [Logger], + exports: [] }) export class QueueModule { } diff --git a/apps/server/src/queue/stats/stats.service.ts b/apps/server/src/queue/stats/stats.service.ts new file mode 100644 index 0000000..d3b91b5 --- /dev/null +++ b/apps/server/src/queue/stats/stats.service.ts @@ -0,0 +1,70 @@ +import { InjectQueue } from "@nestjs/bullmq"; +import { Injectable } from "@nestjs/common"; +import EventBus from "@server/utils/event-bus"; +import { Queue } from "bullmq"; +import { ObjectType } from "@nicestack/common"; +import { QueueJobType } from "../types"; +@Injectable() +export class StatsService { + constructor( + @InjectQueue('general') private generalQueue: Queue + ) { + EventBus.on("dataChanged", async ({ type, data }) => { + const jobOptions = { + removeOnComplete: true, + jobId: this.generateJobId(type as ObjectType, data) // 使用唯一ID防止重复任务 + }; + switch (type) { + case ObjectType.ENROLLMENT: + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + courseId: data.courseId, + type: ObjectType.ENROLLMENT + }, + jobOptions + ); + break; + + case ObjectType.LECTURE: + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + sectionId: data.sectionId, + courseId: data.courseId, + type: ObjectType.LECTURE + }, + jobOptions + ); + break; + + case ObjectType.POST: + if (data.courseId) { + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + courseId: data.courseId, + type: ObjectType.POST + }, + jobOptions + ); + } + break; + } + }); + } + + private generateJobId(type: ObjectType, data: any): string { + // 根据类型和相关ID生成唯一的job标识 + switch (type) { + case ObjectType.ENROLLMENT: + return `stats_${type}_${data.courseId}`; + case ObjectType.LECTURE: + return `stats_${type}_${data.courseId}_${data.sectionId}`; + case ObjectType.POST: + return `stats_${type}_${data.courseId}`; + default: + return `stats_${type}_${Date.now()}`; + } + } +} \ No newline at end of file diff --git a/apps/server/src/queue/types.ts b/apps/server/src/queue/types.ts new file mode 100755 index 0000000..db4f92c --- /dev/null +++ b/apps/server/src/queue/types.ts @@ -0,0 +1,4 @@ +export enum QueueJobType { + UPDATE_STATS = "update_stats", + FILE_PROCESS = "file_process" +} diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts new file mode 100644 index 0000000..404bf2e --- /dev/null +++ b/apps/server/src/queue/worker/file.processor.ts @@ -0,0 +1,26 @@ +import { Job } from 'bullmq'; +import { Logger } from '@nestjs/common'; +import { QueueJobType } from '../types'; +import { ResourceProcessingPipeline } from '@server/models/resource/pipe/resource.pipeline'; +import { GeneralProcessor } from '@server/models/resource/processor/GeneralProcessor'; +import { ImageProcessor } from '@server/models/resource/processor/ImageProcessor'; +import superjson from 'superjson-cjs'; +import { Resource } from '@nicestack/common'; +const logger = new Logger('FileProcessorWorker'); +const pipeline = new ResourceProcessingPipeline() + .addProcessor(new GeneralProcessor()) + .addProcessor(new ImageProcessor()) +export default async function processJob(job: Job) { + if (job.name === QueueJobType.FILE_PROCESS) { + console.log(job) + const { resource } = job.data; + if (!resource) { + throw new Error('No resource provided in job data'); + } + + + const result = await pipeline.execute(resource); + + return result; + } +} \ No newline at end of file diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index ca8efb4..30c27cf 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -1,19 +1,49 @@ import { Job } from 'bullmq'; import { Logger } from '@nestjs/common'; -import { CustomJobType } from '../job.interface'; -import { PushService } from '@server/queue/push/push.service'; +import { + updateCourseLectureStats, + updateSectionLectureStats +} from '@server/models/lecture/utils'; +import { ObjectType } from '@nicestack/common'; +import { + updateCourseEnrollmentStats, + updateCourseReviewStats +} from '@server/models/course/utils'; +import { QueueJobType } from '../types'; +const logger = new Logger('QueueWorker'); +export default async function processJob(job: Job) { + try { + if (job.name === QueueJobType.UPDATE_STATS) { + const { sectionId, courseId, type } = job.data; + // 处理 section 统计 + if (sectionId) { + await updateSectionLectureStats(sectionId); + logger.debug(`Updated section stats for sectionId: ${sectionId}`); + } + // 如果没有 courseId,提前返回 + if (!courseId) { + return; + } + // 处理 course 相关统计 + switch (type) { + case ObjectType.LECTURE: + await updateCourseLectureStats(courseId); + break; + case ObjectType.ENROLLMENT: + await updateCourseEnrollmentStats(courseId); + break; + case ObjectType.POST: + await updateCourseReviewStats(courseId); + break; + default: + logger.warn(`Unknown update stats type: ${type}`); + } -const logger = new Logger("QueueWorker"); + logger.debug(`Updated course stats for courseId: ${courseId}, type: ${type}`); + } -const pushService = new PushService() -export default async function (job: Job) { - switch (job.name) { - case "pushMessage": - - logger.log(`push message ${job.data.id}`) - pushService.messagePush(job.data.registerToken, job.data.messageContent) - break - + } catch (error: any) { + logger.error(`Error processing stats update job: ${error.message}`, error.stack); } } \ No newline at end of file diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index b8c2e3a..ac97d20 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -5,7 +5,6 @@ import { TaxonomyRouter } from '@server/models/taxonomy/taxonomy.router'; import { TermRouter } from '@server/models/term/term.router'; import { TrpcService } from '@server/trpc/trpc.service'; import * as trpcExpress from '@trpc/server/adapters/express'; -import { AuthRouter } from '@server/auth/auth.router'; import ws, { WebSocketServer } from 'ws'; import { AppConfigRouter } from '@server/models/app-config/app-config.router'; import { MessageRouter } from '@server/models/message/message.router'; @@ -30,7 +29,6 @@ export class TrpcRouter { private readonly role: RoleRouter, private readonly rolemap: RoleMapRouter, private readonly transform: TransformRouter, - private readonly auth: AuthRouter, private readonly app_config: AppConfigRouter, private readonly message: MessageRouter, private readonly visitor: VisitRouter, @@ -40,7 +38,7 @@ export class TrpcRouter { // private readonly websocketService: WebSocketService ) { } appRouter = this.trpc.router({ - auth: this.auth.router, + transform: this.transform.router, post: this.post.router, department: this.department.router, diff --git a/apps/server/src/upload/types.ts b/apps/server/src/upload/types.ts new file mode 100644 index 0000000..018c513 --- /dev/null +++ b/apps/server/src/upload/types.ts @@ -0,0 +1,32 @@ +export interface HookRequest { + Type: 'post-finish' | 'pre-create'; + Event: { + Upload: { + ID?: string; + MetaData?: { + filename?: string + }; + Size?: number; + Storage?: { + Path: string + Type: string + }; + }; + }; +} +export interface HookResponse { + RejectUpload?: boolean; + HTTPResponse?: { + StatusCode?: number; + Body?: string; + Headers?: Record; + }; + ChangeFileInfo?: { + ID?: string; + MetaData?: Record; + }; +} +export interface FileHandle { + filename: string + path: string +} \ No newline at end of file diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts new file mode 100644 index 0000000..eb7028e --- /dev/null +++ b/apps/server/src/upload/upload.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Logger, Post, Body, Res } from "@nestjs/common"; +import { Response } from "express"; +import { HookRequest, HookResponse } from "./types"; +import { UploadService } from "./upload.service"; + +@Controller("upload") +export class UploadController { + private readonly logger = new Logger(UploadController.name); + constructor(private readonly uploadService: UploadService) { } + @Post("hook") + async handleTusHook(@Body() hookRequest: HookRequest, @Res() res: Response) { + try { + const hookResponse = await this.uploadService.handleTusHook(hookRequest); + return res.status(200).json(hookResponse); + } catch (error: any) { + this.logger.error(`Error handling hook: ${error.message}`); + return res.status(500).json({ error: 'Internal server error' }); + } + } +} \ No newline at end of file diff --git a/apps/server/src/upload/upload.module.ts b/apps/server/src/upload/upload.module.ts new file mode 100644 index 0000000..b1d37cb --- /dev/null +++ b/apps/server/src/upload/upload.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { UploadController } from './upload.controller'; +import { UploadService } from './upload.service'; +import { BullModule } from '@nestjs/bullmq'; +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 + }), + ], + controllers: [UploadController], + providers: [UploadService], +}) +export class UploadModule { } \ No newline at end of file diff --git a/apps/server/src/upload/upload.service.ts b/apps/server/src/upload/upload.service.ts new file mode 100644 index 0000000..e40ae48 --- /dev/null +++ b/apps/server/src/upload/upload.service.ts @@ -0,0 +1,107 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Queue, Job } from 'bullmq'; // 添加 Job 导入 +import { InjectQueue } from '@nestjs/bullmq'; +import { db, Resource, ResourceType } from '@nicestack/common'; +import { HookRequest, HookResponse } from './types'; +import dayjs from 'dayjs'; +import { getFilenameWithoutExt } from '@server/utils/file'; +import { QueueJobType } from '@server/queue/types'; +import { nanoid } from 'nanoid'; +import { slugify } from 'transliteration'; +import path from 'path'; +import fs from 'node:fs'; +@Injectable() +export class UploadService { + private readonly logger = new Logger(UploadService.name); + constructor( + @InjectQueue('file-queue') private fileQueue: Queue + ) { } + + async handlePreCreateHook(hookRequest: HookRequest): Promise { + const hookResponse: HookResponse = { + HTTPResponse: { + Headers: {} + } + }; + const metaData = hookRequest.Event.Upload.MetaData; + const isValid = metaData && 'filename' in metaData; + if (!isValid) { + hookResponse.RejectUpload = true; + hookResponse.HTTPResponse.StatusCode = 400; + hookResponse.HTTPResponse.Body = 'no filename provided'; + hookResponse.HTTPResponse.Headers['X-Some-Header'] = 'yes'; + } else { + const timestamp = dayjs().format('YYMMDDHHmmss'); + const originalName = metaData.filename; + const extension = path.extname(originalName); // 获取文件扩展名 + // 清理并转换文件名(不包含扩展名) + const cleanName = slugify(getFilenameWithoutExt(originalName), { + lowercase: true, + separator: '-', + trim: true + }) + .replace(/[^\w-]/g, '') + .replace(/-+/g, '-') + .substring(0, 32); + + const uniqueId = nanoid(6); + const fileId = `${timestamp}_${cleanName}_${uniqueId}`; + // await fs.promises.mkdir(path.join(process.env.UPLOAD_DIR, fileId), { recursive: true }); + hookResponse.ChangeFileInfo = { + ID: `${fileId}/${fileId}${extension}`, + MetaData: { + filename: originalName, + normalized_name: cleanName, + folder: fileId + } + }; + } + return hookResponse; + } + + async handlePostFinishHook(hookRequest: HookRequest) { + const { ID, Size, Storage, MetaData } = hookRequest.Event.Upload; + const filename = MetaData?.filename; + + const resource = await db.resource.create({ + data: { + filename, + fileId: ID, + title: getFilenameWithoutExt(filename), // 使用没有扩展名的标题 + metadata: MetaData || {} + } + }) + await this.addToProcessorPipe(resource) + this.logger.log(`Upload ${ID} (${Size} bytes) is finished.`); + } + async handleTusHook(hookRequest: HookRequest): Promise { + const hookResponse: HookResponse = { + HTTPResponse: { + Headers: {} + } + }; + try { + if (hookRequest.Type === 'pre-create') { + return this.handlePreCreateHook(hookRequest); + } + if (hookRequest.Type === 'post-finish') { + this.handlePostFinishHook(hookRequest); + } + return hookResponse; + } catch (error: any) { + this.logger.error(`Error handling hook: ${error.message}`); + throw error; + } + } + async addToProcessorPipe(resource: Resource): Promise { // 修改返回类型为 Job + const job = await this.fileQueue.add(QueueJobType.FILE_PROCESS, { + resource, + timestamp: Date.now() + }, { + attempts: 3, + removeOnComplete: true, + jobId: resource.id + }); + return job; + } +} \ No newline at end of file diff --git a/apps/server/src/utils/file.ts b/apps/server/src/utils/file.ts new file mode 100644 index 0000000..a9e24fc --- /dev/null +++ b/apps/server/src/utils/file.ts @@ -0,0 +1,71 @@ + +import { createHash } from 'crypto'; +import { createReadStream } from 'fs'; +import path from 'path'; +import * as dotenv from 'dotenv'; +dotenv.config(); +export function getFilenameWithoutExt(filename: string) { + return filename ? filename.replace(/\.[^/.]+$/, '') : filename; +} +export function extractFilePathFromUri(uri: string): string { + // 从 /uploads/ 路径中提取文件路径 + return uri.replace('/uploads/', ''); +} +/** + * 计算文件的 SHA-256 哈希值 + * @param filePath 文件路径 + * @returns Promise 返回文件的哈希值(十六进制字符串) + */ +export async function calculateFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + // 创建一个 SHA-256 哈希对象 + const hash = createHash('sha256'); + // 创建文件读取流 + const readStream = createReadStream(filePath); + // 处理读取错误 + readStream.on('error', (error) => { + reject(new Error(`Failed to read file: ${error.message}`)); + }); + // 处理哈希计算错误 + hash.on('error', (error) => { + reject(new Error(`Failed to calculate hash: ${error.message}`)); + }); + // 流式处理文件内容 + readStream + .pipe(hash) + .on('finish', () => { + // 获取最终的哈希值(十六进制格式) + const fileHash = hash.digest('hex'); + resolve(fileHash); + }) + .on('error', (error) => { + reject(new Error(`Hash calculation failed: ${error.message}`)); + }); + }); +} + +/** + * 计算 Buffer 的 SHA-256 哈希值 + * @param buffer 要计算哈希的 Buffer + * @returns string 返回 Buffer 的哈希值(十六进制字符串) + */ +export function calculateBufferHash(buffer: Buffer): string { + const hash = createHash('sha256'); + hash.update(buffer); + return hash.digest('hex'); +} + +/** + * 计算字符串的 SHA-256 哈希值 + * @param content 要计算哈希的字符串 + * @returns string 返回字符串的哈希值(十六进制字符串) + */ +export function calculateStringHash(content: string): string { + const hash = createHash('sha256'); + hash.update(content); + return hash.digest('hex'); +} +export const getUploadFilePath = (fileId: string): string => { + const uploadDirectory = process.env.UPLOAD_DIR; + return path.join(uploadDirectory, fileId); +}; \ No newline at end of file diff --git a/apps/web/entrypoint.sh b/apps/web/entrypoint.sh deleted file mode 100755 index 647c384..0000000 --- a/apps/web/entrypoint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -# 使用envsubst替换index.html中的环境变量占位符 -envsubst < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp -mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html -# 运行serve来提供静态文件 -exec nginx -g "daemon off;" -# 使用 sed 替换 index.html 中的环境变量占位符 -# for var in $(env | cut -d= -f1); do -# sed -i "s|\${$var}|$(eval echo \$$var)|g" /usr/share/nginx/html/index.html -# done - -# # 运行 nginx 来提供静态文件 -# exec nginx -g "daemon off;" \ No newline at end of file diff --git a/apps/web/index.html b/apps/web/index.html index db84405..d1e0ce8 100755 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -12,7 +12,7 @@ VITE_APP_VERSION: "$VITE_APP_VERSION", }; - 烽火慕课 + 烽火mooc diff --git a/apps/web/package.json b/apps/web/package.json index 6fc4320..06df590 100755 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,8 @@ "antd": "^5.19.3", "axios": "^1.7.2", "browser-image-compression": "^2.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "dayjs": "^1.11.12", "framer-motion": "^11.15.0", "idb-keyval": "^6.2.1", @@ -54,6 +56,7 @@ "react-resizable": "^3.0.5", "react-router-dom": "^6.24.1", "superjson": "^2.2.1", + "tailwind-merge": "^2.6.0", "yjs": "^13.6.20", "zod": "^3.23.8" }, diff --git a/apps/web/src/app/main/course/editor/page.tsx b/apps/web/src/app/main/course/editor/page.tsx index 8ba41fe..6603f60 100644 --- a/apps/web/src/app/main/course/editor/page.tsx +++ b/apps/web/src/app/main/course/editor/page.tsx @@ -1,5 +1,8 @@ import CourseEditor from "@web/src/components/models/course/manage/CourseEditor"; +import { useParams } from "react-router-dom"; export function CourseEditorPage() { - return + const { id } = useParams(); + console.log('Course ID:', id); + return } \ No newline at end of file diff --git a/apps/web/src/app/main/course/page.tsx b/apps/web/src/app/main/course/page.tsx index bdb9347..3ed549f 100644 --- a/apps/web/src/app/main/course/page.tsx +++ b/apps/web/src/app/main/course/page.tsx @@ -1,6 +1,6 @@ -import { CourseCard } from "@web/src/components/models/course/course-card" -import { CourseDetail } from "@web/src/components/models/course/course-detail" -import { CourseSyllabus } from "@web/src/components/models/course/course-syllabus" +import { CourseCard } from "@web/src/components/models/course/card/CourseCard" +import { CourseDetail } from "@web/src/components/models/course/detail/course-detail" +import { CourseSyllabus } from "@web/src/components/models/course/detail/course-syllabus" export const CoursePage = () => { // 假设这些数据从API获取 diff --git a/apps/web/src/app/main/courses/instructor/page.tsx b/apps/web/src/app/main/courses/instructor/page.tsx new file mode 100644 index 0000000..be9ef14 --- /dev/null +++ b/apps/web/src/app/main/courses/instructor/page.tsx @@ -0,0 +1,55 @@ +import { PlusIcon } from "@heroicons/react/24/outline"; +import { CourseList } from "@web/src/components/models/course/list/course-list"; +import { Button } from "@web/src/components/presentation/element/Button"; +import { api } from "@nicestack/client"; +import { useState } from "react"; +import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@web/src/providers/auth-provider"; + +export default function InstructorCoursesPage() { + const navigate = useNavigate() + const [currentPage, setCurrentPage] = useState(1); + const { user } = useAuth() + + const { data: paginationRes, refetch } = api.course.findManyWithPagination.useQuery({ + page: currentPage, + pageSize: 8, + where: { + instructors: { + some: { + instructorId: user?.id + } + } + } + }); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + refetch() + }; + + return ( +
+
+
+

我教授的课程

+ +
+ } + /> +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx deleted file mode 100644 index c3c2048..0000000 --- a/apps/web/src/app/main/courses/page.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { motion } from "framer-motion"; -import { useState } from "react"; -import { CourseDto } from "@nicestack/common"; -import { EmptyStateIllustration } from "@web/src/components/presentation/EmptyStateIllustration"; -import { useNavigate } from "react-router-dom"; - -interface CourseCardProps { - course: CourseDto; - type: "created" | "enrolled"; -} - -const CourseCard = ({ course, type }: CourseCardProps) => { - return ( - - {/* Course Thumbnail */} -
- -
-
- - {course.level} - -
-
- - {/* Course Info */} -
-

- {course.title} -

- {course.subTitle && ( -

- {course.subTitle} -

- )} -
- - {/* Course Stats */} -
-
- {course.totalLectures} lectures - - {course.totalDuration} mins -
-
- - - {course.averageRating.toFixed(1)} - -
-
- - {/* Progress Bar (Only for enrolled courses) */} - {type === "enrolled" && course.enrollments[0] && ( -
-
- -
-

- {course.enrollments[0].completionRate}% Complete -

-
- )} - - ); -}; -export default function CoursesPage() { - const [activeTab, setActiveTab] = useState<"enrolled" | "created">("enrolled"); - const [courses, setCourses] = useState([]); - const navigate = useNavigate() - const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.05, - duration: 0.3 - }, - }, - }; - - return ( -
-
- {/* Header */} -
-
-

- 我的课程 -

-
- -
-
- {activeTab === "created" && ( - { - navigate("/course/manage") - }} - transition={{ duration: 0.2 }} - className="flex items-center gap-2 rounded-lg bg-blue-500 px-6 py-3 text-sm font-medium text-white shadow-sm transition-all duration-200 hover:bg-blue-600 hover:shadow-md" - > - - - - - - 创建课程 - - )} -
- - {/* Course Grid */} - - {courses.map((course) => ( - - ))} - - - {/* Empty State */} - {courses.length === 0 && ( - - -

- No courses found -

-

- {activeTab === "enrolled" - ? "You haven't enrolled in any courses yet." - : "You haven't created any courses yet."} -

-
- )} -
-
- ); -} \ No newline at end of file diff --git a/apps/web/src/app/main/courses/student/page.tsx b/apps/web/src/app/main/courses/student/page.tsx new file mode 100644 index 0000000..a93ec87 --- /dev/null +++ b/apps/web/src/app/main/courses/student/page.tsx @@ -0,0 +1,44 @@ +import { CourseList } from "@web/src/components/models/course/list/course-list"; +import { api } from "@nicestack/client"; +import { useState } from "react"; +import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; +import { useAuth } from "@web/src/providers/auth-provider"; + +export default function StudentCoursesPage() { + const [currentPage, setCurrentPage] = useState(1); + const { user } = useAuth() + + const { data: paginationRes, refetch } = api.course.findManyWithPagination.useQuery({ + page: currentPage, + pageSize: 8, + where: { + enrollments: { + some: { + studentId: user?.id + } + } + } + }); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + refetch() + }; + + return ( +
+
+
+

我参加的课程

+
+ }> + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx new file mode 100644 index 0000000..a895b50 --- /dev/null +++ b/apps/web/src/app/main/home/page.tsx @@ -0,0 +1,79 @@ +import React, { useCallback, useState } from 'react'; +import * as tus from 'tus-js-client'; + +const UploadTest: React.FC = () => { + const [progress, setProgress] = useState(0); + const [uploadStatus, setUploadStatus] = useState(''); + + const handleFileSelect = useCallback((event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // 创建 tus upload 实例 + const upload = new tus.Upload(file, { + endpoint: 'http://localhost:8080/files', // 替换成你的 NestJS 服务器地址 + retryDelays: [0, 3000, 5000], + metadata: { + filename: file.name, + filetype: file.type + }, + onError: (error) => { + console.error('上传失败:', error); + setUploadStatus('上传失败'); + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + setProgress(Number(percentage)); + setUploadStatus('上传中...'); + }, + onSuccess: () => { + setUploadStatus('上传成功!'); + console.log('上传完成:', upload.url); + } + }); + + // 开始上传 + upload.start(); + }, []); + + return ( +
+

文件上传测试

+ + + {progress > 0 && ( +
+
上传进度: {progress}%
+
+
+
+
+ )} + + {uploadStatus && ( +
+ 状态: {uploadStatus} +
+ )} +
+ ); +}; + + +export default function HomePage() { + return +} \ No newline at end of file diff --git a/apps/web/src/components/button/excel-importer.tsx b/apps/web/src/components/button/excel-importer.tsx deleted file mode 100755 index 1086856..0000000 --- a/apps/web/src/components/button/excel-importer.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { Button, message } from "antd"; -import { useMemo, useRef, useState } from "react"; -import { Buffer } from "buffer"; -import { useTransform } from "@nicestack/client"; -import { SizeType } from "antd/es/config-provider/SizeContext"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { api } from "@nicestack/client" -export function ExcelImporter({ - type = "trouble", - className, - name = "导入", - taxonomyId, - parentId, - size = "small", - domainId, - refresh = true, - disabled = false, - ghost = true, -}: { - disabled?: boolean; - type?: "trouble" | "term" | "dept" | "staff"; - className?: string; - name?: string; - domainId?: string; - taxonomyId?: string; - parentId?: string; - size?: SizeType; - refresh?: boolean; - ghost?: boolean; -}) { - const fileInput = useRef(null); - const [file, setFile] = useState(); - const [loading, setLoading] = useState(false); - const { user } = useAuth(); - const { importTrouble, importTerms, importDepts, importStaffs } = - useTransform(); - const utils = api.useUtils() - // const queryKey = getQueryKey(api.trouble); - // const domainId = useMemo(() => { - // if (staff && staff?.domainId) return staff?.domainId; - // }, [staff]); - return ( -
- - { - const files = Array.from(e.target.files || []); - if (!files.length) return; // 如果没有文件被选中, 直接返回 - - const file = files[0]; - if (file) { - const isExcel = - file.type === "application/vnd.ms-excel" || - file.type === - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || - file.type === "application/wps-office.xlsx"; - - if (!isExcel) { - message.warning("请选择Excel文件"); - return; - } - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const bufferToBase64 = buffer.toString("base64"); - - try { - setLoading(true); - let data = undefined; - if (type === "trouble") { - data = await importTrouble.mutateAsync({ - base64: bufferToBase64, - domainId, - }); - } - if (type === "term") { - data = await importTerms.mutateAsync({ - base64: bufferToBase64, - domainId, - taxonomyId, - parentId, - }); - } - if (type === "dept") { - data = await importDepts.mutateAsync({ - base64: bufferToBase64, - domainId, - parentId, - }); - } - if (type === "staff") { - data = await importStaffs.mutateAsync({ - base64: bufferToBase64, - domainId, - }); - } - // const data = res.data; - console.log(`%cdata:${data}`, "color:red"); - if (!data?.error) { - - message.success(`已经导入${data.count}条数据`); - utils.trouble.invalidate() - - if (refresh && type !== "trouble") { - setTimeout(() => { - window.location.reload(); - }, 700); - } - } else { - console.log( - `%cerror:${JSON.stringify(data.error)}`, - "color:red" - ); - console.log(JSON.stringify(data.error)); - message.error(JSON.stringify(data.error)); - } - } catch (error) { - console.error(`${error}`); - message.error(`${error}`); - } finally { - if (fileInput.current) { - fileInput.current.value = ""; // 清空文件输入 - } - setLoading(false); - } - } - }} - style={{ display: "none" }} - /> -
- ); -} diff --git a/apps/web/src/components/layout/main/MainLayout.tsx b/apps/web/src/components/layout/main/MainLayout.tsx index 887bb4c..7134d37 100644 --- a/apps/web/src/components/layout/main/MainLayout.tsx +++ b/apps/web/src/components/layout/main/MainLayout.tsx @@ -3,9 +3,10 @@ import { motion, AnimatePresence } from 'framer-motion'; import { TopNavBar } from '@web/src/components/layout/main/top-nav-bar'; import { navItems, notificationItems } from '@web/src/components/layout/main/nav-data'; import { Sidebar } from '@web/src/components/layout/main/side-bar'; +import { Outlet } from 'react-router-dom'; -export function MainLayout({ children }: { children: ReactNode }) { +export function MainLayout() { const [sidebarOpen, setSidebarOpen] = useState(true); const [notifications, setNotifications] = useState(3); const [recentSearches] = useState([ @@ -32,7 +33,7 @@ export function MainLayout({ children }: { children: ReactNode }) { className={`pt-16 min-h-screen transition-all duration-300 ${sidebarOpen ? 'ml-64' : 'ml-0' }`} > - {children} +
); diff --git a/apps/web/src/components/layout/main/nav-data.tsx b/apps/web/src/components/layout/main/nav-data.tsx index fe7c7d0..d4192b9 100644 --- a/apps/web/src/components/layout/main/nav-data.tsx +++ b/apps/web/src/components/layout/main/nav-data.tsx @@ -6,16 +6,18 @@ import { Cog6ToothIcon, BellIcon, HeartIcon, - AcademicCapIcon + AcademicCapIcon, + UsersIcon, + PresentationChartBarIcon } from '@heroicons/react/24/outline'; export const navItems: NavItem[] = [ - { icon: , label: '探索', path: '/' }, - { icon: , label: '我的课程', path: '/courses' }, - { icon: , label: '学习社区', path: '/community' }, + { icon: , label: '探索知识', path: '/' }, + { icon: , label: '我的学习', path: '/courses/student' }, + { icon: , label: '我的授课', path: '/courses/instructor' }, + { icon: , label: '学习社区', path: '/community' }, { icon: , label: '应用设置', path: '/settings' }, ]; - export const notificationItems = [ { icon: , diff --git a/apps/web/src/components/layout/main/top-nav-bar.tsx b/apps/web/src/components/layout/main/top-nav-bar.tsx index 9ea15cd..d1a4e7f 100644 --- a/apps/web/src/components/layout/main/top-nav-bar.tsx +++ b/apps/web/src/components/layout/main/top-nav-bar.tsx @@ -1,4 +1,3 @@ -import { useState, useRef } from 'react'; import { NotificationsDropdown } from './notifications-dropdown'; import { SearchBar } from './search-bar'; @@ -12,7 +11,6 @@ interface TopNavBarProps { notificationItems: Array; recentSearches: string[]; } - export function TopNavBar({ sidebarOpen, setSidebarOpen, @@ -26,12 +24,11 @@ export function TopNavBar({
-

- LearnHub +

+ 烽火mooc

diff --git a/apps/web/src/components/models/course/card/CourseHeader.tsx b/apps/web/src/components/models/course/card/CourseHeader.tsx index 3444487..b6fe3cf 100644 --- a/apps/web/src/components/models/course/card/CourseHeader.tsx +++ b/apps/web/src/components/models/course/card/CourseHeader.tsx @@ -29,11 +29,11 @@ export const CourseHeader = ({
)}
-

{title}

+

{title}

{subTitle && ( -

{subTitle}

+

{subTitle}

)} -
+
{level && (
diff --git a/apps/web/src/components/models/course/card/CourseStats.tsx b/apps/web/src/components/models/course/card/CourseStats.tsx index 7d5fe02..71c416b 100644 --- a/apps/web/src/components/models/course/card/CourseStats.tsx +++ b/apps/web/src/components/models/course/card/CourseStats.tsx @@ -14,15 +14,15 @@ export const CourseStats = ({ totalDuration, }: CourseStatsProps) => { return ( -
+
{averageRating !== undefined && (
-
+
{averageRating.toFixed(1)}
-
+
{numberOfReviews} reviews
@@ -32,10 +32,10 @@ export const CourseStats = ({
-
+
{completionRate}%
-
+
Completion
@@ -45,10 +45,10 @@ export const CourseStats = ({
-
+
{Math.floor(totalDuration / 60)}h {totalDuration % 60}m
-
+
Duration
diff --git a/apps/web/src/components/models/course/list/course-list.tsx b/apps/web/src/components/models/course/list/course-list.tsx index 61e55e9..912954c 100644 --- a/apps/web/src/components/models/course/list/course-list.tsx +++ b/apps/web/src/components/models/course/list/course-list.tsx @@ -2,10 +2,18 @@ import { motion } from "framer-motion"; import { Course, CourseDto } from "@nicestack/common"; import { EmptyState } from "@web/src/components/presentation/space/Empty"; +import { Pagination } from "@web/src/components/presentation/element/Pagination"; + interface CourseListProps { - courses: CourseDto[]; - activeTab: "enrolled" | "created"; + courses?: CourseDto[]; + renderItem: (course: CourseDto) => React.ReactNode; + emptyComponent?: React.ReactNode; + // 新增分页相关属性 + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; } + const container = { hidden: { opacity: 0 }, show: { @@ -20,23 +28,35 @@ export const CourseList = ({ courses, renderItem, emptyComponent: EmptyComponent, -}: CourseListProps & { - renderItem?: (course: CourseDto) => React.ReactNode; - emptyComponent?: React.ReactNode; -}) => { - if (courses.length === 0) { - return EmptyComponent || ( - - ); + currentPage, + totalPages, + onPageChange, +}: CourseListProps) => { + if (!courses || courses.length === 0) { + return EmptyComponent || ; } + + return ( - - {courses.map((course) => renderItem(course))} - +
+ + {courses.map((course) => ( + + {renderItem(course)} + + ))} + + + +
); }; \ No newline at end of file diff --git a/apps/web/src/components/models/course/manage/CourseBasicForm.tsx b/apps/web/src/components/models/course/manage/CourseBasicForm.tsx index f7cef6f..23fd165 100644 --- a/apps/web/src/components/models/course/manage/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/manage/CourseBasicForm.tsx @@ -1,10 +1,11 @@ import { SubmitHandler, useFormContext } from 'react-hook-form'; -import { CourseFormData, useCourseForm } from './CourseEditorContext'; -import { CourseLevel } from '@nicestack/common'; +import { CourseFormData, useCourseEditor } from './CourseEditorContext'; +import { CourseLevel, CourseLevelLabel } from '@nicestack/common'; import { FormInput } from '@web/src/components/presentation/form/FormInput'; import { FormSelect } from '@web/src/components/presentation/form/FormSelect'; import { FormArrayField } from '@web/src/components/presentation/form/FormArrayField'; +import { convertToOptions } from '@nicestack/client'; export function CourseBasicForm() { const { register, formState: { errors }, watch, handleSubmit } = useFormContext(); @@ -19,15 +20,7 @@ export function CourseBasicForm() { type="textarea" placeholder="请输入课程描述" /> - + {/* */} ); diff --git a/apps/web/src/components/models/course/manage/CourseEditor.tsx b/apps/web/src/components/models/course/manage/CourseEditor.tsx index 9fbfc5a..bc2dfe8 100644 --- a/apps/web/src/components/models/course/manage/CourseEditor.tsx +++ b/apps/web/src/components/models/course/manage/CourseEditor.tsx @@ -3,8 +3,8 @@ import { CourseBasicForm } from "./CourseBasicForm"; import { CourseFormProvider } from "./CourseEditorContext"; import CourseEditorLayout from "./CourseEditorLayout"; -export default function CourseEditor() { - return +export default function CourseEditor({ id }: { id?: string }) { + return diff --git a/apps/web/src/components/models/course/manage/CourseEditorContext.tsx b/apps/web/src/components/models/course/manage/CourseEditorContext.tsx index adb30c1..961c2ab 100644 --- a/apps/web/src/components/models/course/manage/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/manage/CourseEditorContext.tsx @@ -1,60 +1,97 @@ -import { createContext, useContext, ReactNode } from 'react'; +import { createContext, useContext, ReactNode, useEffect } from 'react'; import { useForm, FormProvider, SubmitHandler } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; -import { CourseLevel, CourseStatus } from '@nicestack/common'; -import { useCourse } from '@nicestack/client'; - +import { CourseDto, CourseLevel, CourseStatus } from '@nicestack/common'; +import { api, useCourse } from '@nicestack/client'; +import toast from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; // 定义课程表单验证 Schema const courseSchema = z.object({ title: z.string().min(1, '课程标题不能为空'), - subTitle: z.string().optional(), - description: z.string().optional(), - thumbnail: z.string().url().optional(), + subTitle: z.string().nullish(), + description: z.string().nullish(), + thumbnail: z.string().url().nullish(), level: z.nativeEnum(CourseLevel), - requirements: z.array(z.string()).optional(), - objectives: z.array(z.string()).optional(), - skills: z.array(z.string()).optional(), - audiences: z.array(z.string()).optional(), + requirements: z.array(z.string()).nullish(), + objectives: z.array(z.string()).nullish(), + skills: z.array(z.string()).nullish(), + audiences: z.array(z.string()).nullish(), status: z.nativeEnum(CourseStatus), }); export type CourseFormData = z.infer; interface CourseEditorContextType { onSubmit: SubmitHandler; + editId?: string; // 添加 editId + course?: CourseDto +} +interface CourseFormProviderProps { + children: ReactNode; + editId?: string; // 添加 editId 参数 } const CourseEditorContext = createContext(null); -export function CourseFormProvider({ children }: { children: ReactNode }) { - const { create } = useCourse() +export function CourseFormProvider({ children, editId }: CourseFormProviderProps) { + const { create, update } = useCourse() + const { data: course }: { data: CourseDto } = api.course.findFirst.useQuery({ where: { id: editId } }, { enabled: Boolean(editId) }) const methods = useForm({ resolver: zodResolver(courseSchema), defaultValues: { status: CourseStatus.DRAFT, level: CourseLevel.BEGINNER, - requirements: [], objectives: [], skills: [], audiences: [], }, }); - + const navigate = useNavigate() + useEffect(() => { + if (course) { + // 只选择表单需要的字段 + const formData = { + title: course.title, + subTitle: course.subTitle, + description: course.description, + thumbnail: course.thumbnail, + level: course.level, + requirements: course.requirements, + objectives: course.objectives, + skills: course.skills, + audiences: course.audiences, + status: course.status, + }; + methods.reset(formData as any); + } + }, [course, methods]); const onSubmit: SubmitHandler = async (data: CourseFormData) => { try { - // TODO: 实现API调用 - console.log('Form data:', data); - await create.mutateAsync({ - data: { - ...data - } - }) + if (editId) { + await update.mutateAsync({ + where: { id: editId }, + data: { + ...data + } + }) + toast.success('课程更新成功!'); + } else { + const result = await create.mutateAsync({ + data: { + ...data + } + }) + console.log(`/course/${result.id}/manage`) + navigate(`/course/${result.id}/manage`, { replace: true }) + toast.success('课程创建成功!'); + } } catch (error) { console.error('Error submitting form:', error); + toast.error('操作失败,请重试!'); } }; return ( - + {children} @@ -62,10 +99,10 @@ export function CourseFormProvider({ children }: { children: ReactNode }) { ); } -export const useCourseForm = () => { +export const useCourseEditor = () => { const context = useContext(CourseEditorContext); if (!context) { - throw new Error('useCourseForm must be used within CourseFormProvider'); + throw new Error('useCourseEditor must be used within CourseFormProvider'); } return context; }; \ No newline at end of file diff --git a/apps/web/src/components/models/course/manage/CourseEditorHeader.tsx b/apps/web/src/components/models/course/manage/CourseEditorHeader.tsx index 4326c9f..9f93f91 100644 --- a/apps/web/src/components/models/course/manage/CourseEditorHeader.tsx +++ b/apps/web/src/components/models/course/manage/CourseEditorHeader.tsx @@ -1,12 +1,20 @@ import { ArrowLeftIcon, ClockIcon } from '@heroicons/react/24/outline'; import { SubmitHandler, useFormContext } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { CourseFormData, useCourseForm } from './CourseEditorContext'; - +import { CourseFormData, useCourseEditor } from './CourseEditorContext'; +import { Button } from '@web/src/components/presentation/element/Button'; +import { CourseStatus, CourseStatusLabel } from '@nicestack/common'; +import Tag from '@web/src/components/presentation/element/Tag'; +const courseStatusVariant: Record = { + [CourseStatus.DRAFT]: 'default', + [CourseStatus.UNDER_REVIEW]: 'warning', + [CourseStatus.PUBLISHED]: 'success', + [CourseStatus.ARCHIVED]: 'danger' +}; export default function CourseEditorHeader() { const navigate = useNavigate(); - const { handleSubmit} = useFormContext() - const { onSubmit } = useCourseForm() + const { handleSubmit, formState: { isValid, isDirty, errors } } = useFormContext() + const { onSubmit, course } = useCourseEditor() return (
@@ -18,17 +26,25 @@ export default function CourseEditorHeader() {
-

UI设计入门课程

- 审核中 -
- - 总时长 12:30:00 -
+

{course?.title || '新建课程'}

+ + {course?.status ? CourseStatusLabel[course.status] : CourseStatusLabel[CourseStatus.DRAFT]} + + {course?.totalDuration ? ( +
+ + 总时长 {course?.totalDuration} +
+ ) : null}
- +
); diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index d968f97..b5d28a9 100644 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -1,5 +1,5 @@ import { TreeSelect, TreeSelectProps } from "antd"; -import React, { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback, useRef } from "react"; import { getUniqueItems } from "@nicestack/common"; import { api } from "@nicestack/client"; import { DefaultOptionType } from "antd/es/select"; @@ -15,6 +15,7 @@ interface TermSelectProps { taxonomyId?: string; disabled?: boolean; className?: string; + domainId?: string; } export default function TermSelect({ @@ -25,6 +26,7 @@ export default function TermSelect({ placeholder = "选择单位", multiple = false, taxonomyId, + domainId, // rootId = null, disabled = false, // domain = undefined, @@ -43,6 +45,7 @@ export default function TermSelect({ return await utils.term.getParentSimpleTree.fetch({ termIds: idsArray, taxonomyId, + domainId, }); } catch (error) { console.error( @@ -61,6 +64,7 @@ export default function TermSelect({ try { const rootDepts = await utils.term.getChildSimpleTree.fetch({ taxonomyId, + domainId, }); let combinedDepts = [...rootDepts]; if (defaultValue) { @@ -106,6 +110,7 @@ export default function TermSelect({ const result = await utils.term.getChildSimpleTree.fetch({ termIds: [id], taxonomyId, + domainId, }); const newItems = getUniqueItems([...listTreeData, ...result], "id"); setListTreeData(newItems); @@ -137,6 +142,7 @@ export default function TermSelect({ const expandedNodes = await utils.term.getChildSimpleTree.fetch({ termIds: allKeyIds, taxonomyId, + domainId, }); const flattenedNodes = expandedNodes.flat(); const newItems = getUniqueItems( @@ -163,6 +169,12 @@ export default function TermSelect({ disabled={disabled} showSearch allowClear + // ref={selectRef} + dropdownStyle={{ + width: "300px", // 固定宽度 + minWidth: "200px", // 最小宽度 + maxWidth: "600px", // 最大宽度 + }} defaultValue={defaultValue} value={value} className={className} @@ -178,4 +190,4 @@ export default function TermSelect({ onDropdownVisibleChange={handleDropdownVisibleChange} /> ); -} +} \ No newline at end of file diff --git a/apps/web/src/components/presentation/EmptyStateIllustration.tsx b/apps/web/src/components/presentation/EmptyStateIllustration.tsx index 8c58482..47dd9f8 100644 --- a/apps/web/src/components/presentation/EmptyStateIllustration.tsx +++ b/apps/web/src/components/presentation/EmptyStateIllustration.tsx @@ -1,6 +1,11 @@ import { motion } from "framer-motion"; export const EmptyStateIllustration = () => { + const springTransition = { + type: "spring", + stiffness: 100, + damping: 10 + }; return ( { viewBox="0 0 240 200" fill="none" xmlns="http://www.w3.org/2000/svg" - initial={{ opacity: 0, scale: 0.9 }} - animate={{ opacity: 1, scale: 1 }} - transition={{ duration: 0.5, ease: "easeOut" }} + initial="hidden" + animate="visible" + variants={{ + hidden: { opacity: 0, scale: 0.8 }, + visible: { + opacity: 1, + scale: 1, + transition: { + when: "beforeChildren", + staggerChildren: 0.1 + } + } + }} + transition={springTransition} > - {/* Background Elements */} - - {/* Books Stack */} + {/* 书籍堆叠 - 使用更现代的配色方案 */} - {/* Bottom Book */} + {/* 底部书籍 */} - {/* Middle Book */} + {/* 中间书籍 */} - {/* Top Book */} + {/* 顶部书籍 */} - {/* Floating Elements */} + {/* Magnifying Glass */} - {/* Small Circles */} - - - - + + - {/* Decorative Lines */} + {/* Atom Structure */} + + + + + + + {/* Mathematical Symbols */} + + + + π + + + + {/* Connection Lines */} + + ); -}; +}; \ No newline at end of file diff --git a/apps/web/src/components/presentation/container/Card.tsx b/apps/web/src/components/presentation/container/Card.tsx index cc21462..f06a73a 100644 --- a/apps/web/src/components/presentation/container/Card.tsx +++ b/apps/web/src/components/presentation/container/Card.tsx @@ -13,10 +13,10 @@ export const Card = ({ children, className = '', hover = true, onClick }: CardPr void; + className?: string; +} + +export function AnimatedTabs({ options, activeTab, onChange, className = "" }: AnimatedTabsProps) { + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/presentation/element/Button.tsx b/apps/web/src/components/presentation/element/Button.tsx new file mode 100644 index 0000000..b6f07c6 --- /dev/null +++ b/apps/web/src/components/presentation/element/Button.tsx @@ -0,0 +1,101 @@ +import { HTMLMotionProps, motion } from 'framer-motion'; +import { forwardRef, ReactNode } from 'react'; +import { cn } from '@web/src/utils/classname'; +import { LoadingOutlined } from '@ant-design/icons'; + +export interface ButtonProps extends Omit, keyof React.ButtonHTMLAttributes> { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + fullWidth?: boolean; + className?: string; + disabled?: boolean; + children?: ReactNode; + // 添加常用的按钮事件处理器 + onClick?: (event: React.MouseEvent) => void; + onMouseEnter?: (event: React.MouseEvent) => void; + onMouseLeave?: (event: React.MouseEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; +} +export const Button = forwardRef( + ( + { + className, + variant = 'primary', + size = 'md', + isLoading = false, + disabled, + leftIcon, + rightIcon, + children, + fullWidth, + ...props + }, + ref + ) => { + const variants = { + primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm disabled:bg-gray-400', + secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 disabled:bg-gray-200 disabled:text-gray-500', + outline: 'border-2 border-gray-300 hover:bg-gray-50 disabled:border-gray-200 disabled:text-gray-400', + ghost: 'hover:bg-gray-100 disabled:bg-transparent disabled:text-gray-400', + danger: 'bg-red-600 text-white hover:bg-red-700 disabled:bg-gray-400', + }; + + const ringColors = { + primary: 'focus:ring-blue-500', + secondary: 'focus:ring-gray-500', + outline: 'focus:ring-gray-400', + ghost: 'focus:ring-gray-400', + danger: 'focus:ring-red-500', + }; + + + const sizes = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-base', + lg: 'px-6 py-3 text-lg', + }; + + return ( + + {isLoading && ( + + ); + } +); + +Button.displayName = 'Button'; \ No newline at end of file diff --git a/apps/web/src/components/presentation/element/Pagination.tsx b/apps/web/src/components/presentation/element/Pagination.tsx new file mode 100644 index 0000000..0c1cac5 --- /dev/null +++ b/apps/web/src/components/presentation/element/Pagination.tsx @@ -0,0 +1,106 @@ +import { motion } from 'framer-motion'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + maxVisiblePages?: number; + className?: string; +} + +export const Pagination = ({ + currentPage, + totalPages, + onPageChange, + maxVisiblePages = 7, + className = '', +}: PaginationProps) => { + const getVisiblePages = () => { + if (totalPages <= maxVisiblePages) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + + const leftSiblingIndex = Math.max(currentPage - 1, 1); + const rightSiblingIndex = Math.min(currentPage + 1, totalPages); + + const shouldShowLeftDots = leftSiblingIndex > 2; + const shouldShowRightDots = rightSiblingIndex < totalPages - 1; + + if (!shouldShowLeftDots && shouldShowRightDots) { + const leftRange = Array.from({ length: maxVisiblePages - 1 }, (_, i) => i + 1); + return [...leftRange, '...', totalPages]; + } + + if (shouldShowLeftDots && !shouldShowRightDots) { + const rightRange = Array.from( + { length: maxVisiblePages - 1 }, + (_, i) => totalPages - (maxVisiblePages - 2) + i + ); + return [1, '...', ...rightRange]; + } + + if (shouldShowLeftDots && shouldShowRightDots) { + const middleRange = Array.from( + { length: maxVisiblePages - 4 }, + (_, i) => leftSiblingIndex + i + ); + return [1, '...', ...middleRange, '...', totalPages]; + } + + return Array.from({ length: totalPages }, (_, i) => i + 1); + }; + + const visiblePages = getVisiblePages(); + + return ( + + onPageChange(currentPage - 1)} + disabled={currentPage <= 1} + className="px-4 py-2 rounded-md bg-white border border-gray-200 hover:bg-gray-50 + disabled:opacity-50 disabled:cursor-not-allowed transition-colors + shadow-sm text-gray-700 font-medium" + > + 上一页 + + +
+ {visiblePages.map((page, index) => ( + typeof page === 'number' && onPageChange(page)} + className={`px-4 py-2 rounded-md font-medium transition-colors ${currentPage === page + ? 'bg-blue-500 text-white shadow-md' + : page === '...' + ? 'cursor-default' + : 'bg-white border border-gray-200 hover:bg-gray-50 text-gray-700 shadow-sm' + }`} + > + {page} + + ))} +
+ + onPageChange(currentPage + 1)} + disabled={currentPage >= totalPages} + className="px-4 py-2 rounded-md bg-white border border-gray-200 hover:bg-gray-50 + disabled:opacity-50 disabled:cursor-not-allowed transition-colors + shadow-sm text-gray-700 font-medium" + > + 下一页 + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/components/presentation/element/Tag.tsx b/apps/web/src/components/presentation/element/Tag.tsx new file mode 100644 index 0000000..c8c91f4 --- /dev/null +++ b/apps/web/src/components/presentation/element/Tag.tsx @@ -0,0 +1,88 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import { motion } from 'framer-motion' +import { FC, ReactNode } from 'react' + +const tagVariants = cva( + 'inline-flex items-center justify-center rounded-full px-3 py-1 text-sm font-medium transition-all', + { + variants: { + variant: { + default: 'bg-gray-100 text-gray-800 hover:bg-gray-200', + primary: 'bg-blue-100 text-blue-800 hover:bg-blue-200', + success: 'bg-green-100 text-green-800 hover:bg-green-200', + warning: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200', + danger: 'bg-red-100 text-red-800 hover:bg-red-200', + }, + size: { + sm: 'text-xs px-2 py-0.5', + md: 'text-sm px-3 py-1', + lg: 'text-base px-4 py-1.5', + }, + interactive: { + true: 'cursor-pointer', + }, + removable: { + true: 'pr-2', + }, + }, + defaultVariants: { + variant: 'default', + size: 'md', + interactive: false, + removable: false, + }, + } +) + +interface TagProps extends VariantProps { + children: ReactNode + onClick?: () => void + onRemove?: () => void + className?: string +} + +const Tag: FC = ({ + children, + variant, + size, + interactive, + removable, + onClick, + onRemove, + className, +}) => { + return ( + + {children} + {removable && ( + { + e.stopPropagation() + onRemove?.() + }} + className="ml-1.5 rounded-full p-0.5 hover:bg-black/5" + > + + + + + )} + + ) +} + +export default Tag \ No newline at end of file diff --git a/apps/web/src/components/presentation/form/FormSelect.tsx b/apps/web/src/components/presentation/form/FormSelect.tsx index 77d2f7a..7b0795d 100644 --- a/apps/web/src/components/presentation/form/FormSelect.tsx +++ b/apps/web/src/components/presentation/form/FormSelect.tsx @@ -98,7 +98,11 @@ export function FormSelect({ name, label, options, placeholder = '请选择' }: className={`p-2 cursor-pointer flex items-center justify-between ${value === option.value ? 'bg-blue-50 text-blue-600' : 'hover:bg-gray-50'}`} onClick={() => { - setValue(name, option.value); + setValue(name, option.value, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true + }); setIsOpen(false); }} > diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 16b36ec..453bec4 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -13,11 +13,12 @@ import RoleAdminPage from "../app/admin/role/page"; import WithAuth from "../components/utils/with-auth"; import LoginPage from "../app/login"; import BaseSettingPage from "../app/admin/base-setting/page"; - -import CoursesPage from "../app/main/courses/page"; import { CoursePage } from "../app/main/course/page"; import { CourseEditorPage } from "../app/main/course/editor/page"; import { MainLayout } from "../components/layout/main/MainLayout"; +import StudentCoursesPage from "../app/main/courses/student/page"; +import InstructorCoursesPage from "../app/main/courses/instructor/page"; +import HomePage from "../app/main/home/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; @@ -50,15 +51,32 @@ export const routes: CustomRouteObject[] = [ }, children: [ { - path: "courses", - index: true, - element: + element: , + children: [ + { + index: true, + element: + }, + { + path: "courses", + children: [ + { + path: "student", + element: + }, + { + path: "instructor", + element: + } + ] + } + ] }, { path: "course", children: [{ - path: "manage", - element: + path: ":id?/manage", // 使用 ? 表示 id 参数是可选的 + element: }] }, { diff --git a/apps/web/src/utils/classname.ts b/apps/web/src/utils/classname.ts new file mode 100644 index 0000000..4834418 --- /dev/null +++ b/apps/web/src/utils/classname.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} \ No newline at end of file diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf new file mode 100644 index 0000000..4c078e4 --- /dev/null +++ b/config/nginx/conf.d/web.conf @@ -0,0 +1,93 @@ +server { + listen 80; + server_name 192.168.12.77; + + # 基础优化配置 + tcp_nopush on; + tcp_nodelay on; + types_hash_max_size 2048; + + # Gzip 压缩配置 + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + text/javascript; + + # 默认首页配置 + location / { + root /usr/share/nginx/html; + index index.html index.htm; + + # 文件缓存配置 + open_file_cache max=1000 inactive=20s; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + open_file_cache_errors on; + + try_files $uri $uri/ /index.html; + } + + # 文件上传处理配置 + location /uploads/ { + alias /data/uploads/; + + # 文件传输优化 + sendfile on; + tcp_nopush on; + aio on; + directio 512; + + # 认证配置 + auth_request /auth-file; + auth_request_set $auth_status $upstream_status; + auth_request_set $auth_user_id $upstream_http_x_user_id; + auth_request_set $auth_resource_type $upstream_http_x_resource_type; + + # 缓存控制 + expires 0; + add_header Cache-Control "private, no-transform"; + add_header X-User-Id $auth_user_id; + add_header X-Resource-Type $auth_resource_type; + + # 带宽控制 + limit_rate 102400k; + limit_rate_after 100m; + + # CORS 配置 + add_header 'Access-Control-Allow-Origin' '$http_origin' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' + 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' + always; + } + + # 认证服务配置 + location = /auth-file { + internal; + proxy_pass http://192.168.12.77:3000/auth/file; + + # 请求优化 + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + + # 请求信息传递 + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Original-Method $request_method; + proxy_set_header Host $host; + proxy_set_header X-Query-Params $query_string; + } +} \ No newline at end of file diff --git a/config/nginx/conf.d/web.template b/config/nginx/conf.d/web.template new file mode 100644 index 0000000..bce5ac0 --- /dev/null +++ b/config/nginx/conf.d/web.template @@ -0,0 +1,93 @@ +server { + listen 80; + server_name ${SERVER_IP}; + + # 基础优化配置 + tcp_nopush on; + tcp_nodelay on; + types_hash_max_size 2048; + + # Gzip 压缩配置 + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + text/javascript; + + # 默认首页配置 + location / { + root /usr/share/nginx/html; + index index.html index.htm; + + # 文件缓存配置 + open_file_cache max=1000 inactive=20s; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + open_file_cache_errors on; + + try_files $uri $uri/ /index.html; + } + + # 文件上传处理配置 + location /uploads/ { + alias /data/uploads/; + + # 文件传输优化 + sendfile on; + tcp_nopush on; + aio on; + directio 512; + + # 认证配置 + auth_request /auth-file; + auth_request_set $auth_status $upstream_status; + auth_request_set $auth_user_id $upstream_http_x_user_id; + auth_request_set $auth_resource_type $upstream_http_x_resource_type; + + # 缓存控制 + expires 0; + add_header Cache-Control "private, no-transform"; + add_header X-User-Id $auth_user_id; + add_header X-Resource-Type $auth_resource_type; + + # 带宽控制 + limit_rate 102400k; + limit_rate_after 100m; + + # CORS 配置 + add_header 'Access-Control-Allow-Origin' '$http_origin' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' + 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' + always; + } + + # 认证服务配置 + location = /auth-file { + internal; + proxy_pass http://${SERVER_IP}:3000/auth/file; + + # 请求优化 + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + + # 请求信息传递 + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Original-Method $request_method; + proxy_set_header Host $host; + proxy_set_header X-Query-Params $query_string; + } +} \ No newline at end of file diff --git a/config/nginx/entrypoint.sh b/config/nginx/entrypoint.sh new file mode 100755 index 0000000..ca557f8 --- /dev/null +++ b/config/nginx/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# 处理所有 .template 结尾的文件 +for template in /etc/nginx/conf.d/*.template; do + [ ! -f "$template" ] && echo "No template files found" && continue + # 将输出文件名改为 .conf 结尾 + conf="${template%.template}.conf" + echo "Processing $template" + if envsubst '$SERVER_IP' < "$template" > "$conf"; then + echo "Replaced $conf successfully" + else + echo "Failed to replace $conf" + fi +done +# Check if the index.html file exists before processing +if [ -f "/usr/share/nginx/html/index.html" ]; then + # Use envsubst to replace environment variable placeholders + envsubst < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp + mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html +else + echo "Info: /usr/share/nginx/html/index.html does not exist , skip replace env" + exit 1 +fi + +# Run nginx to serve static files +exec nginx -g "daemon off;" \ No newline at end of file diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf new file mode 100755 index 0000000..271c72e --- /dev/null +++ b/config/nginx/nginx.conf @@ -0,0 +1,32 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main + '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + # include /etc/nginx/conf.d/*.conf; + include /etc/nginx/conf.d/web.conf; + # include /etc/nginx/conf.d/file.conf; +} \ No newline at end of file diff --git a/packages/client/src/api/hooks/useCourse.ts b/packages/client/src/api/hooks/useCourse.ts index 6718491..5061072 100644 --- a/packages/client/src/api/hooks/useCourse.ts +++ b/packages/client/src/api/hooks/useCourse.ts @@ -11,38 +11,45 @@ export function useCourse() { // Mutations create: api.course.create.useMutation({ onSuccess: () => { + utils.course.invalidate() utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }), update: api.course.update.useMutation({ onSuccess: () => { utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }), createMany: api.course.createMany.useMutation({ onSuccess: () => { utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }), deleteMany: api.course.deleteMany.useMutation({ onSuccess: () => { utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }), softDeleteByIds: api.course.softDeleteByIds.useMutation({ onSuccess: () => { utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }), updateOrder: api.course.updateOrder.useMutation({ onSuccess: () => { utils.course.findMany.invalidate(); utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate() }, }) }; diff --git a/packages/client/src/tools/objects.ts b/packages/client/src/tools/objects.ts index 92527f3..8b8b03f 100644 --- a/packages/client/src/tools/objects.ts +++ b/packages/client/src/tools/objects.ts @@ -19,3 +19,15 @@ export function mergeIfDefined( return acc; }, { ...obj1 }); // 使用对象展开运算符创建新对象,确保原始对象不被修改 } + +interface Option { + label: string; + value: T; +} + +export function convertToOptions(obj: Record): Option[] { + return (Object.entries(obj) as [T, string][]).map(([value, label]) => ({ + label, + value + })); +} \ No newline at end of file diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 72831eb..bdde991 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -107,6 +107,7 @@ model Staff { registerToken String? enrollments Enrollment[] teachedCourses CourseInstructor[] + ownedResources Resource[] @@index([officerId]) @@index([deptId]) @@ -305,6 +306,7 @@ model Course { terms Term[] @relation("course_term") // 课程学期 instructors CourseInstructor[] // 课程讲师团队 sections Section[] // 课程章节结构 + lectures Lecture[] enrollments Enrollment[] // 学生报名记录 reviews Post[] // 学员课程评价 @@ -378,8 +380,10 @@ model Lecture { // 关联关系 resources Resource[] - section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade) - sectionId String @map("section_id") + section Section? @relation(fields: [sectionId], references: [id], onDelete: Cascade) + sectionId String? @map("section_id") + course Course? @relation(fields: [courseId], references: [id], onDelete: Cascade) + courseId String? @map("course_id") comments Post[] visits Visit[] @@ -426,24 +430,41 @@ model CourseInstructor { } model Resource { - id String @id @default(cuid()) @map("id") - title String @map("title") - description String? @map("description") - type String @map("type") - url String @map("url") - fileType String? @map("file_type") - fileSize Int? @map("file_size") - downloadCount Int @default(0) @map("download_count") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) @map("id") + title String? @map("title") + description String? @map("description") + type String? @map("type") + // 存储信息 + filename String? + fileId String? + url String? + hash String? + // 元数据 + metadata Json? @map("metadata") + // 处理状态控制 + processStatus String? - lectures Lecture[] - posts Post[] + // 审计字段 + createdAt DateTime? @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") + createdBy String? @map("created_by") + updatedBy String? @map("updated_by") + deletedAt DateTime? @map("deleted_at") + isPublic Boolean? @default(true) @map("is_public") + owner Staff? @relation(fields: [ownerId], references: [id]) + ownerId String? @map("owner_id") + post Post? @relation(fields: [postId], references: [id]) + postId String? @map("post_id") + lecture Lecture? @relation(fields: [lectureId], references: [id]) + lectureId String? @map("lecture_id") + // 索引 @@index([type]) + @@index([createdAt]) @@map("resource") } + model Node { id String @id @default(cuid()) @map("id") title String @map("title") diff --git a/packages/common/src/db.ts b/packages/common/src/db.ts index 7390d70..e2a4943 100755 --- a/packages/common/src/db.ts +++ b/packages/common/src/db.ts @@ -15,5 +15,4 @@ export const db = (() => { } return prisma; } - throw new Error('PrismaClient is not available in browser environment'); })() as PrismaClient; \ No newline at end of file diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 800dc46..1581857 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -4,6 +4,7 @@ export enum SocketMsgType { export enum PostType { POST = "post", POST_COMMENT = "post_comment", + COURSE_REVIEW = "course_review" } export enum TaxonomySlug { CATEGORY = "category", @@ -16,6 +17,20 @@ export enum VisitType { } +export enum StorageProvider { + LOCAL = 'LOCAL', + S3 = 'S3', + OSS = 'OSS', + COS = 'COS', + CDN = 'CDN' +} + +export enum ResourceProcessStatus { + PENDING = 'PENDING', + PROCESSING = 'PROCESSING', + SUCCESS = 'SUCCESS', + FAILED = 'FAILED', +} export enum ObjectType { DEPARTMENT = "department", STAFF = "staff", @@ -72,7 +87,6 @@ export enum RolePerms { MANAGE_ANY_ROLE = "MANAGE_ANY_ROLE", MANAGE_DOM_ROLE = "MANAGE_DOM_ROLE", } - export enum AppConfigSlug { BASE_SETTING = "base_setting", } @@ -90,7 +104,6 @@ export enum ResourceType { ZIP = "zip", // 压缩包文件 OTHER = "other" // 其他未分类资源 } - // 课程等级的枚举,描述了不同学习水平的课程 export enum CourseLevel { BEGINNER = "beginner", // 初级课程,适合初学者 @@ -114,6 +127,12 @@ export enum CourseStatus { PUBLISHED = "published", // 已发布的课程,可以被学员报名学习 ARCHIVED = "archived" // 已归档的课程,不再对外展示 } +export const CourseStatusLabel: Record = { + [CourseStatus.DRAFT]: "草稿", + [CourseStatus.UNDER_REVIEW]: "审核中", + [CourseStatus.PUBLISHED]: "已发布", + [CourseStatus.ARCHIVED]: "已归档" +}; // 报名状态的枚举,描述了用户报名参加课程的不同状态 export enum EnrollmentStatus { @@ -128,4 +147,45 @@ export enum EnrollmentStatus { export enum InstructorRole { MAIN = "main", // 主讲教师 ASSISTANT = "assistant" // 助教 -} \ No newline at end of file +} +export const EnrollmentStatusLabel = { + [EnrollmentStatus.PENDING]: '待处理', + [EnrollmentStatus.ACTIVE]: '进行中', + [EnrollmentStatus.COMPLETED]: '已完成', + [EnrollmentStatus.CANCELLED]: '已取消', + [EnrollmentStatus.REFUNDED]: '已退款' +}; + +export const InstructorRoleLabel = { + [InstructorRole.MAIN]: '主讲教师', + [InstructorRole.ASSISTANT]: '助教' +}; + +export const ResourceTypeLabel = { + [ResourceType.VIDEO]: '视频', + [ResourceType.PDF]: 'PDF文档', + [ResourceType.DOC]: 'Word文档', + [ResourceType.EXCEL]: 'Excel表格', + [ResourceType.PPT]: 'PPT演示文稿', + [ResourceType.CODE]: '代码文件', + [ResourceType.LINK]: '链接', + [ResourceType.IMAGE]: '图片', + [ResourceType.AUDIO]: '音频', + [ResourceType.ZIP]: '压缩包', + [ResourceType.OTHER]: '其他' +}; + +export const CourseLevelLabel = { + [CourseLevel.BEGINNER]: '初级', + [CourseLevel.INTERMEDIATE]: '中级', + [CourseLevel.ADVANCED]: '高级', + [CourseLevel.ALL_LEVELS]: '不限级别' +}; + +export const LessonTypeLabel = { + [LessonType.VIDEO]: '视频课程', + [LessonType.ARTICLE]: '图文课程', + [LessonType.QUIZ]: '测验', + [LessonType.ASSIGNMENT]: '作业' +}; + diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index 1a8c67b..fa9f5f3 100755 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -246,6 +246,7 @@ export const TermMethodSchema = { termIds: z.array(z.string().nullish()).nullish(), parentId: z.string().nullish(), taxonomyId: z.string().nullish(), + domainId: z.string().nullish(), }), findMany: z.object({ keyword: z.string().nullish(), diff --git a/packages/iconer/types/src/generated/icon-names.d.ts b/packages/iconer/types/src/generated/icon-names.d.ts index 1df58f0..2ed0d3c 100644 --- a/packages/iconer/types/src/generated/icon-names.d.ts +++ b/packages/iconer/types/src/generated/icon-names.d.ts @@ -1 +1 @@ -export type IconName = 'align-center' | 'align-justify' | 'align-left' | 'align-right' | 'arrow-drop-down' | 'bold' | 'check' | 'content' | 'copy' | 'edit' | 'get-text' | 'home' | 'horizontal-rule' | 'image' | 'italic' | 'link-off' | 'link' | 'logout' | 'react' | 'redo' | 'share' | 'strike' | 'text-indent' | 'text-outdent' | 'underline' | 'undo' | 'zoomin' | 'zoomout'; +export type IconName = 'account-location' | 'add' | 'admin-outlined' | 'airport' | 'align-center' | 'align-justify' | 'align-left' | 'align-right' | 'approve' | 'arrow-drop-down' | 'blocks-group' | 'bold' | 'caret-right' | 'category-outline' | 'check-one' | 'check' | 'config' | 'content' | 'copy' | 'cube-duotone' | 'date-time' | 'delete' | 'edit' | 'error-duotone' | 'error-outline' | 'exit' | 'filter' | 'fluent-person' | 'get-text' | 'group-work' | 'health-circle' | 'history' | 'home' | 'horizontal-rule' | 'image' | 'inbox' | 'italic' | 'link-off' | 'link' | 'list' | 'logout' | 'loop' | 'more' | 'note' | 'number-symbol' | 'org' | 'people-32' | 'people-group' | 'people-plus' | 'people' | 'person-board' | 'person-hair' | 'person-home' | 'plane-takeoff' | 'plane' | 'progress' | 'radar-chart' | 'react' | 'redo' | 'right-line' | 'seal-check' | 'search' | 'setting' | 'share' | 'strike' | 'subject-rounded' | 'sum' | 'target' | 'text-indent' | 'text-outdent' | 'time' | 'underline' | 'undo' | 'user-id' | 'work' | 'zoomin' | 'zoomout'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c888aee..8c5408e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,15 @@ importers: '@trpc/server': specifier: 11.0.0-rc.456 version: 11.0.0-rc.456 + '@tus/file-store': + specifier: ^1.5.1 + version: 1.5.1 + '@tus/s3-store': + specifier: ^1.6.2 + version: 1.6.2 + '@tus/server': + specifier: ^1.10.0 + version: 1.10.0 argon2: specifier: ^0.41.1 version: 0.41.1 @@ -64,6 +73,9 @@ importers: exceljs: specifier: ^4.4.0 version: 4.4.0 + fluent-ffmpeg: + specifier: ^2.1.3 + version: 2.1.3 ioredis: specifier: ^5.4.1 version: 5.4.1 @@ -76,6 +88,9 @@ importers: lodash.debounce: specifier: ^4.0.8 version: 4.0.8 + mime-types: + specifier: ^2.1.35 + version: 2.1.35 minio: specifier: ^8.0.1 version: 8.0.1 @@ -88,12 +103,18 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + sharp: + specifier: ^0.33.5 + version: 0.33.5 socket.io: specifier: ^4.7.5 version: 4.7.5 superjson-cjs: specifier: ^2.2.3 version: 2.2.3 + transliteration: + specifier: ^2.3.5 + version: 2.3.5 tus-js-client: specifier: ^4.1.0 version: 4.1.0 @@ -128,9 +149,15 @@ importers: '@types/express': specifier: ^4.17.21 version: 4.17.21 + '@types/fluent-ffmpeg': + specifier: ^2.1.27 + version: 2.1.27 '@types/jest': specifier: ^29.5.2 version: 29.5.12 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/multer': specifier: ^1.4.12 version: 1.4.12 @@ -287,6 +314,12 @@ importers: browser-image-compression: specifier: ^2.0.2 version: 2.0.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 dayjs: specifier: ^1.11.12 version: 1.11.13 @@ -323,6 +356,9 @@ importers: superjson: specifier: ^2.2.1 version: 2.2.1 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 yjs: specifier: ^13.6.20 version: 13.6.21 @@ -627,6 +663,169 @@ packages: peerDependencies: react: '>=16.9.0' + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.717.0': + resolution: {integrity: sha512-jzaH8IskAXVnqlZ3/H/ROwrB2HCnq/atlN7Hi7FIfjWvMPf5nfcJKfzJ1MXFX0EQR5qO6X4TbK7rgi7Bjw9NjQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-sso-oidc@3.716.0': + resolution: {integrity: sha512-lA4IB9FzR2KjH7EVCo+mHGFKqdViVyeBQEIX9oVratL/l7P0bMS1fMwgfHOc3ACazqNxBxDES7x08ZCp32y6Lw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.716.0 + + '@aws-sdk/client-sso@3.716.0': + resolution: {integrity: sha512-5Nb0jJXce2TclbjG7WVPufwhgV1TRydz1QnsuBtKU0AdViEpr787YrZhPpGnNIM1Dx+R1H/tmAHZnOoohS6D8g==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-sts@3.716.0': + resolution: {integrity: sha512-i4SVNsrdXudp8T4bkm7Fi3YWlRnvXCSwvNDqf6nLqSJxqr4CN3VlBELueDyjBK7TAt453/qSif+eNx+bHmwo4Q==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/core@3.716.0': + resolution: {integrity: sha512-5DkUiTrbyzO8/W4g7UFEqRFpuhgizayHI/Zbh0wtFMcot8801nJV+MP/YMhdjimlvAr/OqYB08FbGsPyWppMTw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-env@3.716.0': + resolution: {integrity: sha512-JI2KQUnn2arICwP9F3CnqP1W3nAbm4+meQg/yOhp9X0DMzQiHrHRd4HIrK2vyVgi2/6hGhONY5uLF26yRTA7nQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-http@3.716.0': + resolution: {integrity: sha512-CZ04pl2z7igQPysQyH2xKZHM3fLwkemxQbKOlje3TmiS1NwXvcKvERhp9PE/H23kOL7beTM19NMRog/Fka/rlw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-ini@3.716.0': + resolution: {integrity: sha512-P37We2GtZvdROxiwP0zrpEL81/HuYK1qlYxp5VCj3uV+G4mG8UQN2gMIU/baYrpOQqa0h81RfyQGRFUjVaDVqw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.716.0 + + '@aws-sdk/credential-provider-node@3.716.0': + resolution: {integrity: sha512-FGQPK2uKfS53dVvoskN/s/t6m0Po24BGd1PzJdzHBFCOjxbZLM6+8mDMXeyi2hCLVVQOUcuW41kOgmJ0+zMbww==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-process@3.716.0': + resolution: {integrity: sha512-0spcu2MWVVHSTHH3WE2E//ttUJPwXRM3BCp+WyI41xLzpNu1Fd8zjOrDpEo0SnGUzsSiRTIJWgkuu/tqv9NJ2A==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-sso@3.716.0': + resolution: {integrity: sha512-J2IA3WuCpRGGoZm6VHZVFCnrxXP+41iUWb9Ct/1spljegTa1XjiaZ5Jf3+Ubj7WKiyvP9/dgz1L0bu2bYEjliw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.716.0': + resolution: {integrity: sha512-vzgpWKs2gGXZGdbMKRFrMW4PqEFWkGvwWH2T7ZwQv9m+8lQ7P4Dk2uimqu0f37HZAbpn8HFMqRh4CaySjU354A==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.716.0 + + '@aws-sdk/middleware-bucket-endpoint@3.714.0': + resolution: {integrity: sha512-I/xSOskiseJJ8i183Z522BgqbgYzLKP7jGcg2Qeib/IWoG2IP+9DH8pwqagKaPAycyswtnoKBJiiFXY43n0CkA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-expect-continue@3.714.0': + resolution: {integrity: sha512-rlzsXdG8Lzo4Qpl35ZnpOBAWlzvDHpP9++0AXoUwAJA0QmMm7auIRmgxJuNj91VwT9h15ZU6xjU4S7fJl4W0+w==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.717.0': + resolution: {integrity: sha512-a5kY5r7/7bDZZlOQQGWOR1ulQewdtNexdW1Ex5DD0FLKlFY7RD0va24hxQ6BP7mWHol+Dx4pj6UQ8ahk0ap1tw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-host-header@3.714.0': + resolution: {integrity: sha512-6l68kjNrh5QC8FGX3I3geBDavWN5Tg1RLHJ2HLA8ByGBtJyCwnz3hEkKfaxn0bBx0hF9DzbfjEOUF6cDqy2Kjg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-location-constraint@3.714.0': + resolution: {integrity: sha512-MX7M+V+FblujKck3fyuzePVIAy9530gY719IiSxV6uN1qLHl7VDJxNblpF/KpXakD6rOg8OpvtmqsXj9aBMftw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-logger@3.714.0': + resolution: {integrity: sha512-RkqHlMvQWUaRklU1bMfUuBvdWwxgUtEqpADaHXlGVj3vtEY2UgBjy+57CveC4MByqKIunNvVHBBbjrGVtwY7Lg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.714.0': + resolution: {integrity: sha512-AVU5ixnh93nqtsfgNc284oXsXaadyHGPHpql/jwgaaqQfEXjS/1/j3j9E/vpacfTTz2Vzo7hAOjnvrOXSEVDaA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.716.0': + resolution: {integrity: sha512-Qzz5OfRA/5brqfvq+JHTInwS1EuJ1+tC6qMtwKWJN3czMnVJVdnnsPTf+G5IM/1yYaGEIjY8rC1ExQLcc8ApFQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-ssec@3.714.0': + resolution: {integrity: sha512-RkK8REAVwNUQmYbIDRw8eYbMJ8F1Rw4C9mlME4BBMhFlelGcD3ErU2ce24moQbDxBjNwHNESmIqgmdQk93CDCQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-user-agent@3.716.0': + resolution: {integrity: sha512-FpAtT6nNKrYdkDZndutEraiRMf+TgDzAGvniqRtZ/YTPA+gIsWrsn+TwMKINR81lFC3nQfb9deS5CFtxd021Ew==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/region-config-resolver@3.714.0': + resolution: {integrity: sha512-HJzsQxgMOAzZrbf/YIqEx30or4tZK1oNAk6Wm6xecUQx+23JXIaePRu1YFUOLBBERQ4QBPpISFurZWBMZ5ibAw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.716.0': + resolution: {integrity: sha512-k0goWotZKKz+kV6Ln0qeAMSeSVi4NipuIIz5R8A0uCF2zBK4CXWdZR7KeaIoLBhJwQnHj1UU7E+2MK74KIUBzA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/token-providers@3.714.0': + resolution: {integrity: sha512-vKN064aLE3kl+Zl16Ony3jltHnMddMBT7JRkP1L+lLywhA0PcAKxpdvComul/sTBWnbnwLnaS5NsDUhcWySH8A==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.714.0 + + '@aws-sdk/types@3.714.0': + resolution: {integrity: sha512-ZjpP2gYbSFlxxaUDa1Il5AVvfggvUPbjzzB/l3q0gIE5Thd6xKW+yzEpt2mLZ5s5UaYSABZbF94g8NUOF4CVGA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-arn-parser@3.693.0': + resolution: {integrity: sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-endpoints@3.714.0': + resolution: {integrity: sha512-Xv+Z2lhe7w7ZZRsgBwBMZgGTVmS+dkkj2S13uNHAx9lhB5ovM8PhK5G/j28xYf6vIibeuHkRAbb7/ozdZIGR+A==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-locate-window@3.693.0': + resolution: {integrity: sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-user-agent-browser@3.714.0': + resolution: {integrity: sha512-OdJJ03cP9/MgIVToPJPCPUImbpZzTcwdIgbXC0tUQPJhbD7b7cB4LdnkhNHko+MptpOrCq4CPY/33EpOjRdofw==} + + '@aws-sdk/util-user-agent-node@3.716.0': + resolution: {integrity: sha512-3PqaXmQbxrtHKAsPCdp7kn5FrQktj8j3YyuNsqFZ8rWZeEQ88GWlsvE61PTsr2peYCKzpFqYVddef2x1axHU0w==} + engines: {node: '>=16.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.709.0': + resolution: {integrity: sha512-2GPCwlNxeHspoK/Mc8nbk9cBOkSpp3j2SJUQmFnyQK6V/pR6II2oPRyZkMomug1Rc10hqlBHByMecq4zhV2uUw==} + engines: {node: '>=16.0.0'} + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -822,6 +1021,9 @@ packages: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emotion/hash@0.8.0': resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} @@ -1228,6 +1430,123 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -1601,6 +1920,10 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@redis/client@1.6.0': + resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} + engines: {node: '>=14'} + '@remix-run/router@1.17.1': resolution: {integrity: sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==} engines: {node: '>=14.0.0'} @@ -1808,6 +2131,10 @@ packages: cpu: [x64] os: [win32] + '@shopify/semaphore@3.1.0': + resolution: {integrity: sha512-LxonkiWEu12FbZhuOMhsdocpxCqm7By8C/2U9QgNuEoXUx2iMrlXjJv3p93RwfNC6TrdlNRo17gRer1z1309VQ==} + engines: {node: '>=18.12.0'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1817,6 +2144,209 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/abort-controller@3.1.9': + resolution: {integrity: sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==} + engines: {node: '>=16.0.0'} + + '@smithy/chunked-blob-reader-native@3.0.1': + resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} + + '@smithy/chunked-blob-reader@4.0.0': + resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} + + '@smithy/config-resolver@3.0.13': + resolution: {integrity: sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==} + engines: {node: '>=16.0.0'} + + '@smithy/core@2.5.6': + resolution: {integrity: sha512-w494xO+CPwG/5B/N2l0obHv2Fi9U4DAY+sTi1GWT3BVvGpZetJjJXAynIO9IHp4zS1PinGhXtRSZydUXbJO4ag==} + engines: {node: '>=16.0.0'} + + '@smithy/credential-provider-imds@3.2.8': + resolution: {integrity: sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-codec@3.1.10': + resolution: {integrity: sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ==} + + '@smithy/eventstream-serde-browser@3.0.14': + resolution: {integrity: sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-config-resolver@3.0.11': + resolution: {integrity: sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-node@3.0.13': + resolution: {integrity: sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-universal@3.0.13': + resolution: {integrity: sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw==} + engines: {node: '>=16.0.0'} + + '@smithy/fetch-http-handler@4.1.2': + resolution: {integrity: sha512-R7rU7Ae3ItU4rC0c5mB2sP5mJNbCfoDc8I5XlYjIZnquyUwec7fEo78F6DA3SmgJgkU1qTMcZJuGblxZsl10ZA==} + + '@smithy/hash-blob-browser@3.1.10': + resolution: {integrity: sha512-elwslXOoNunmfS0fh55jHggyhccobFkexLYC1ZeZ1xP2BTSrcIBaHV2b4xUQOdctrSNOpMqOZH1r2XzWTEhyfA==} + + '@smithy/hash-node@3.0.11': + resolution: {integrity: sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==} + engines: {node: '>=16.0.0'} + + '@smithy/hash-stream-node@3.1.10': + resolution: {integrity: sha512-olomK/jZQ93OMayW1zfTHwcbwBdhcZOHsyWyiZ9h9IXvc1mCD/VuvzbLb3Gy/qNJwI4MANPLctTp2BucV2oU/Q==} + engines: {node: '>=16.0.0'} + + '@smithy/invalid-dependency@3.0.11': + resolution: {integrity: sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@3.0.0': + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} + + '@smithy/md5-js@3.0.11': + resolution: {integrity: sha512-3NM0L3i2Zm4bbgG6Ymi9NBcxXhryi3uE8fIfHJZIOfZVxOkGdjdgjR9A06SFIZCfnEIWKXZdm6Yq5/aPXFFhsQ==} + + '@smithy/middleware-content-length@3.0.13': + resolution: {integrity: sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-endpoint@3.2.7': + resolution: {integrity: sha512-GTxSKf280aJBANGN97MomUQhW1VNxZ6w7HAj/pvZM5MUHbMPOGnWOp1PRYKi4czMaHNj9bdiA+ZarmT3Wkdqiw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-retry@3.0.32': + resolution: {integrity: sha512-v8gVA9HqibuZkFuFpfkC/EcHE8no/3Mv3JvRUGly63Axt4yyas1WDVOasFSdiqm2hZVpY7/k8mRT1Wd5k7r3Yw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-serde@3.0.11': + resolution: {integrity: sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-stack@3.0.11': + resolution: {integrity: sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==} + engines: {node: '>=16.0.0'} + + '@smithy/node-config-provider@3.1.12': + resolution: {integrity: sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==} + engines: {node: '>=16.0.0'} + + '@smithy/node-http-handler@3.3.3': + resolution: {integrity: sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==} + engines: {node: '>=16.0.0'} + + '@smithy/property-provider@3.1.11': + resolution: {integrity: sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==} + engines: {node: '>=16.0.0'} + + '@smithy/protocol-http@4.1.8': + resolution: {integrity: sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==} + engines: {node: '>=16.0.0'} + + '@smithy/querystring-builder@3.0.11': + resolution: {integrity: sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==} + engines: {node: '>=16.0.0'} + + '@smithy/querystring-parser@3.0.11': + resolution: {integrity: sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==} + engines: {node: '>=16.0.0'} + + '@smithy/service-error-classification@3.0.11': + resolution: {integrity: sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==} + engines: {node: '>=16.0.0'} + + '@smithy/shared-ini-file-loader@3.1.12': + resolution: {integrity: sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==} + engines: {node: '>=16.0.0'} + + '@smithy/signature-v4@4.2.4': + resolution: {integrity: sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==} + engines: {node: '>=16.0.0'} + + '@smithy/smithy-client@3.5.2': + resolution: {integrity: sha512-h7xn+1wlpbXyLrtvo/teHR1SFGIIrQ3imzG0nz43zVLAJgvfC1Mtdwa1pFhoIOYrt/TiNjt4pD0gSYQEdZSBtg==} + engines: {node: '>=16.0.0'} + + '@smithy/types@3.7.2': + resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} + engines: {node: '>=16.0.0'} + + '@smithy/url-parser@3.0.11': + resolution: {integrity: sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==} + + '@smithy/util-base64@3.0.0': + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-body-length-browser@3.0.0': + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} + + '@smithy/util-body-length-node@3.0.0': + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@3.0.0': + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-config-provider@3.0.0': + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-defaults-mode-browser@3.0.32': + resolution: {integrity: sha512-FAGsnm/xJ19SZeoqGyo9CosqjUlm+XJTmygDMktebvDKw3bKiIiZ40O1MA6Z52KLmekYU2GO7BEK7u6e7ZORKw==} + engines: {node: '>= 10.0.0'} + + '@smithy/util-defaults-mode-node@3.0.32': + resolution: {integrity: sha512-2CzKhkPFCVdd15f3+0D1rldNlvJME8pVRBtVVsea2hy7lcOn0bGB0dTVUwzgfM4LW/aU4IOg3jWf25ZWaxbOiw==} + engines: {node: '>= 10.0.0'} + + '@smithy/util-endpoints@2.1.7': + resolution: {integrity: sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==} + engines: {node: '>=16.0.0'} + + '@smithy/util-hex-encoding@3.0.0': + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-middleware@3.0.11': + resolution: {integrity: sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==} + engines: {node: '>=16.0.0'} + + '@smithy/util-retry@3.0.11': + resolution: {integrity: sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-stream@3.3.3': + resolution: {integrity: sha512-bOm0YMMxRjbI3X6QkWwADPFkh2AH2xBMQIB1IQgCsCRqXXpSJatgjUR3oxHthpYwFkw3WPkOt8VgMpJxC0rFqg==} + engines: {node: '>=16.0.0'} + + '@smithy/util-uri-escape@3.0.0': + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@3.0.0': + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-waiter@3.2.0': + resolution: {integrity: sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==} + engines: {node: '>=16.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -2019,6 +2549,22 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tus/file-store@1.5.1': + resolution: {integrity: sha512-bEaZ9zPCezpJW0Cr7rfles+gylXZ+0rSGfQvoTHXoTAbwyy4iIytC25njf1PdWkh3JQU0dKIhZuspnAvVOMntA==} + engines: {node: '>=16'} + + '@tus/s3-store@1.6.2': + resolution: {integrity: sha512-u8+CxH8Q0E1Bf3PrmaxOWki826wHrOci2GLEyQ5Lj5UI+CzFOWzhNuq213PI9y2RDY53uizt5qMuXcrAO13bYw==} + engines: {node: '>=16'} + + '@tus/server@1.10.0': + resolution: {integrity: sha512-wiHRlCSq13ApgizYvP17y/utf/ztF6trWmuUnJ92FFTcoztF2MzEnx1gsL2XtSltSbsOfc/S6xT+PqP2x/DGgw==} + engines: {node: '>=16'} + + '@tus/utils@0.5.0': + resolution: {integrity: sha512-SFJC9db7hJ5O9HbvpN9EKJYdjn8cx8tGgwFDDHPRXh+qBNYdb/MK6kh/vN6Rh82QH07D4k4nwGh4jlpugo6F7g==} + engines: {node: '>=16'} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2068,6 +2614,9 @@ packages: '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/fluent-ffmpeg@2.1.27': + resolution: {integrity: sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -2101,6 +2650,9 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -2566,6 +3118,9 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -2645,6 +3200,9 @@ packages: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2795,6 +3353,9 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -2833,6 +3394,10 @@ packages: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -2860,6 +3425,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + combine-errors@3.0.3: resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} @@ -3419,6 +3991,10 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + fast-xml-parser@4.5.0: resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} hasBin: true @@ -3487,6 +4063,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -3568,6 +4148,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3767,6 +4351,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -4444,6 +5031,9 @@ packages: resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} engines: {node: '>= 6.0.0'} + multistream@4.1.0: + resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -5302,6 +5892,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -5326,6 +5921,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5348,6 +5947,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -5537,6 +6139,9 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -5640,6 +6245,11 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + transliteration@2.3.5: + resolution: {integrity: sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==} + engines: {node: '>=6.0.0'} + hasBin: true + traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -5971,6 +6581,10 @@ packages: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6057,6 +6671,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.4.5: resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} engines: {node: '>= 14'} @@ -6279,6 +6896,513 @@ snapshots: resize-observer-polyfill: 1.5.1 throttle-debounce: 5.0.2 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.714.0 + tslib: 2.6.3 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.714.0 + tslib: 2.6.3 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.3 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.3 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.714.0 + tslib: 2.6.3 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.6.3 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.3 + + '@aws-sdk/client-s3@3.717.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-bucket-endpoint': 3.714.0 + '@aws-sdk/middleware-expect-continue': 3.714.0 + '@aws-sdk/middleware-flexible-checksums': 3.717.0 + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-location-constraint': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-sdk-s3': 3.716.0 + '@aws-sdk/middleware-ssec': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/signature-v4-multi-region': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@aws-sdk/xml-builder': 3.709.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/eventstream-serde-browser': 3.0.14 + '@smithy/eventstream-serde-config-resolver': 3.0.11 + '@smithy/eventstream-serde-node': 3.0.13 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-blob-browser': 3.1.10 + '@smithy/hash-node': 3.0.11 + '@smithy/hash-stream-node': 3.1.10 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/md5-js': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.7 + '@smithy/middleware-retry': 3.0.32 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.32 + '@smithy/util-defaults-mode-node': 3.0.32 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-stream': 3.3.3 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.2.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.7 + '@smithy/middleware-retry': 3.0.32 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.32 + '@smithy/util-defaults-mode-node': 3.0.32 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.716.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.7 + '@smithy/middleware-retry': 3.0.32 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.32 + '@smithy/util-defaults-mode-node': 3.0.32 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sts@3.716.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-node': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/middleware-host-header': 3.714.0 + '@aws-sdk/middleware-logger': 3.714.0 + '@aws-sdk/middleware-recursion-detection': 3.714.0 + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/region-config-resolver': 3.714.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@aws-sdk/util-user-agent-browser': 3.714.0 + '@aws-sdk/util-user-agent-node': 3.716.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.6 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.7 + '@smithy/middleware-retry': 3.0.32 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.32 + '@smithy/util-defaults-mode-node': 3.0.32 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.716.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/core': 2.5.6 + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + fast-xml-parser: 4.4.1 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-env@3.716.0': + dependencies: + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-http@3.716.0': + dependencies: + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/node-http-handler': 3.3.3 + '@smithy/property-provider': 3.1.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/util-stream': 3.3.3 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-ini@3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0)': + dependencies: + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/credential-provider-env': 3.716.0 + '@aws-sdk/credential-provider-http': 3.716.0 + '@aws-sdk/credential-provider-process': 3.716.0 + '@aws-sdk/credential-provider-sso': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)) + '@aws-sdk/credential-provider-web-identity': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/types': 3.714.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + + '@aws-sdk/credential-provider-node@3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0)': + dependencies: + '@aws-sdk/credential-provider-env': 3.716.0 + '@aws-sdk/credential-provider-http': 3.716.0 + '@aws-sdk/credential-provider-ini': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/credential-provider-process': 3.716.0 + '@aws-sdk/credential-provider-sso': 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)) + '@aws-sdk/credential-provider-web-identity': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/types': 3.714.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + + '@aws-sdk/credential-provider-process@3.716.0': + dependencies: + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-sso@3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))': + dependencies: + '@aws-sdk/client-sso': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/token-providers': 3.714.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)) + '@aws-sdk/types': 3.714.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.716.0(@aws-sdk/client-sts@3.716.0)': + dependencies: + '@aws-sdk/client-sts': 3.716.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-bucket-endpoint@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.6.3 + + '@aws-sdk/middleware-expect-continue@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-flexible-checksums@3.717.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-stream': 3.3.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@aws-sdk/middleware-host-header@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-location-constraint@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-logger@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-recursion-detection@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-sdk-s3@3.716.0': + dependencies: + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/core': 2.5.6 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-stream': 3.3.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@aws-sdk/middleware-ssec@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-user-agent@3.716.0': + dependencies: + '@aws-sdk/core': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@aws-sdk/util-endpoints': 3.714.0 + '@smithy/core': 2.5.6 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/region-config-resolver@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + tslib: 2.6.3 + + '@aws-sdk/signature-v4-multi-region@3.716.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/token-providers@3.714.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0))': + dependencies: + '@aws-sdk/client-sso-oidc': 3.716.0(@aws-sdk/client-sts@3.716.0) + '@aws-sdk/types': 3.714.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/types@3.714.0': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/util-arn-parser@3.693.0': + dependencies: + tslib: 2.6.3 + + '@aws-sdk/util-endpoints@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/types': 3.7.2 + '@smithy/util-endpoints': 2.1.7 + tslib: 2.6.3 + + '@aws-sdk/util-locate-window@3.693.0': + dependencies: + tslib: 2.6.3 + + '@aws-sdk/util-user-agent-browser@3.714.0': + dependencies: + '@aws-sdk/types': 3.714.0 + '@smithy/types': 3.7.2 + bowser: 2.11.0 + tslib: 2.6.3 + + '@aws-sdk/util-user-agent-node@3.716.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.716.0 + '@aws-sdk/types': 3.714.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@aws-sdk/xml-builder@3.709.0': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -6299,7 +7423,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6487,7 +7611,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - debug: 4.3.5 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6509,6 +7633,11 @@ snapshots: '@ctrl/tinycolor@3.6.1': {} + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.6.3 + optional: true + '@emotion/hash@0.8.0': {} '@emotion/unitless@0.7.5': {} @@ -6674,7 +7803,7 @@ snapshots: '@eslint/config-array@0.19.1': dependencies: '@eslint/object-schema': 2.1.5 - debug: 4.3.5 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6700,7 +7829,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.5 + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.1 @@ -6796,6 +7925,81 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@ioredis/commands@1.2.0': {} '@isaacs/cliui@8.0.2': @@ -7311,6 +8515,13 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + '@redis/client@1.6.0': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + optional: true + '@remix-run/router@1.17.1': {} '@rollup/pluginutils@5.1.0(rollup@4.29.1)': @@ -7426,6 +8637,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.29.1': optional: true + '@shopify/semaphore@3.1.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -7436,6 +8649,337 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@3.1.9': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/chunked-blob-reader-native@3.0.1': + dependencies: + '@smithy/util-base64': 3.0.0 + tslib: 2.6.3 + + '@smithy/chunked-blob-reader@4.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/config-resolver@3.0.13': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + tslib: 2.6.3 + + '@smithy/core@2.5.6': + dependencies: + '@smithy/middleware-serde': 3.0.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-stream': 3.3.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/credential-provider-imds@3.2.8': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + tslib: 2.6.3 + + '@smithy/eventstream-codec@3.1.10': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.7.2 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.6.3 + + '@smithy/eventstream-serde-browser@3.0.14': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.13 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/eventstream-serde-config-resolver@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/eventstream-serde-node@3.0.13': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.13 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/eventstream-serde-universal@3.0.13': + dependencies: + '@smithy/eventstream-codec': 3.1.10 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/fetch-http-handler@4.1.2': + dependencies: + '@smithy/protocol-http': 4.1.8 + '@smithy/querystring-builder': 3.0.11 + '@smithy/types': 3.7.2 + '@smithy/util-base64': 3.0.0 + tslib: 2.6.3 + + '@smithy/hash-blob-browser@3.1.10': + dependencies: + '@smithy/chunked-blob-reader': 4.0.0 + '@smithy/chunked-blob-reader-native': 3.0.1 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/hash-node@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/hash-stream-node@3.1.10': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/invalid-dependency@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.6.3 + + '@smithy/is-array-buffer@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/md5-js@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/middleware-content-length@3.0.13': + dependencies: + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/middleware-endpoint@3.2.7': + dependencies: + '@smithy/core': 2.5.6 + '@smithy/middleware-serde': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-middleware': 3.0.11 + tslib: 2.6.3 + + '@smithy/middleware-retry@3.0.32': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/service-error-classification': 3.0.11 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + tslib: 2.6.3 + uuid: 9.0.1 + + '@smithy/middleware-serde@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/middleware-stack@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/node-config-provider@3.1.12': + dependencies: + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/node-http-handler@3.3.3': + dependencies: + '@smithy/abort-controller': 3.1.9 + '@smithy/protocol-http': 4.1.8 + '@smithy/querystring-builder': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/property-provider@3.1.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/protocol-http@4.1.8': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/querystring-builder@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.6.3 + + '@smithy/querystring-parser@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/service-error-classification@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + + '@smithy/shared-ini-file-loader@3.1.12': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/signature-v4@4.2.4': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/smithy-client@3.5.2': + dependencies: + '@smithy/core': 2.5.6 + '@smithy/middleware-endpoint': 3.2.7 + '@smithy/middleware-stack': 3.0.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-stream': 3.3.3 + tslib: 2.6.3 + + '@smithy/types@3.7.2': + dependencies: + tslib: 2.6.3 + + '@smithy/url-parser@3.0.11': + dependencies: + '@smithy/querystring-parser': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/util-base64@3.0.0': + dependencies: + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-body-length-browser@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/util-body-length-node@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.6.3 + + '@smithy/util-buffer-from@3.0.0': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-config-provider@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/util-defaults-mode-browser@3.0.32': + dependencies: + '@smithy/property-provider': 3.1.11 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + bowser: 2.11.0 + tslib: 2.6.3 + + '@smithy/util-defaults-mode-node@3.0.32': + dependencies: + '@smithy/config-resolver': 3.0.13 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/smithy-client': 3.5.2 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/util-endpoints@2.1.7': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/util-hex-encoding@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/util-middleware@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/util-retry@3.0.11': + dependencies: + '@smithy/service-error-classification': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + + '@smithy/util-stream@3.3.3': + dependencies: + '@smithy/fetch-http-handler': 4.1.2 + '@smithy/node-http-handler': 3.3.3 + '@smithy/types': 3.7.2 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-uri-escape@3.0.0': + dependencies: + tslib: 2.6.3 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.6.3 + + '@smithy/util-utf8@3.0.0': + dependencies: + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-waiter@3.2.0': + dependencies: + '@smithy/abort-controller': 3.1.9 + '@smithy/types': 3.7.2 + tslib: 2.6.3 + '@socket.io/component-emitter@3.1.2': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.24.7)': @@ -7613,6 +9157,39 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tus/file-store@1.5.1': + dependencies: + '@tus/utils': 0.5.0 + debug: 4.4.0 + optionalDependencies: + '@redis/client': 1.6.0 + transitivePeerDependencies: + - supports-color + + '@tus/s3-store@1.6.2': + dependencies: + '@aws-sdk/client-s3': 3.717.0 + '@shopify/semaphore': 3.1.0 + '@tus/utils': 0.5.0 + debug: 4.4.0 + multistream: 4.1.0 + transitivePeerDependencies: + - aws-crt + - supports-color + + '@tus/server@1.10.0': + dependencies: + '@tus/utils': 0.5.0 + debug: 4.4.0 + lodash.throttle: 4.1.1 + optionalDependencies: + '@redis/client': 1.6.0 + ioredis: 5.4.1 + transitivePeerDependencies: + - supports-color + + '@tus/utils@0.5.0': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.7 @@ -7683,6 +9260,10 @@ snapshots: '@types/qs': 6.9.15 '@types/serve-static': 1.15.7 + '@types/fluent-ffmpeg@2.1.27': + dependencies: + '@types/node': 20.14.10 + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 20.14.10 @@ -7719,6 +9300,8 @@ snapshots: '@types/methods@1.1.4': {} + '@types/mime-types@2.1.4': {} + '@types/mime@1.3.5': {} '@types/multer@1.4.12': @@ -7878,7 +9461,7 @@ snapshots: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.3.5 + debug: 4.4.0 eslint: 9.17.0(jiti@1.21.6) typescript: 5.7.2 transitivePeerDependencies: @@ -7915,7 +9498,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.16.0(typescript@5.5.3) '@typescript-eslint/utils': 7.16.0(eslint@8.57.0)(typescript@5.5.3) - debug: 4.3.5 + debug: 4.4.0 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.3) optionalDependencies: @@ -7927,7 +9510,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2) '@typescript-eslint/utils': 8.18.2(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2) - debug: 4.3.5 + debug: 4.4.0 eslint: 9.17.0(jiti@1.21.6) ts-api-utils: 1.3.0(typescript@5.7.2) typescript: 5.7.2 @@ -7959,7 +9542,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.16.0 '@typescript-eslint/visitor-keys': 7.16.0 - debug: 4.3.5 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -7974,7 +9557,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/visitor-keys': 8.18.2 - debug: 4.3.5 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -8379,6 +9962,8 @@ snapshots: asap@2.0.6: {} + async@0.2.10: {} + async@3.2.5: {} asynckit@0.4.0: {} @@ -8501,6 +10086,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.11.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -8654,6 +10241,10 @@ snapshots: cjs-module-lexer@1.3.1: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + classnames@2.5.1: {} cli-cursor@3.1.0: @@ -8688,6 +10279,8 @@ snapshots: clsx@1.2.1: {} + clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} co@4.6.0: {} @@ -8708,6 +10301,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + combine-errors@3.0.3: dependencies: custom-error-instance: 2.1.1 @@ -8920,8 +10523,7 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.0.3: - optional: true + detect-libc@2.0.3: {} detect-newline@3.1.0: {} @@ -9374,6 +10976,10 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.0.5 + fast-xml-parser@4.5.0: dependencies: strnum: 1.0.5 @@ -9451,6 +11057,11 @@ snapshots: flatted@3.3.1: {} + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + follow-redirects@1.15.6: {} for-each@0.3.3: @@ -9531,6 +11142,9 @@ snapshots: function-bind@1.1.2: {} + generic-pool@3.9.0: + optional: true + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9758,6 +11372,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -9836,7 +11452,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.5 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -10593,6 +12209,11 @@ snapshots: type-is: 1.6.18 xtend: 4.0.2 + multistream@4.1.0: + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + mute-stream@0.0.8: {} mute-stream@1.0.0: {} @@ -11514,6 +13135,8 @@ snapshots: semver@7.6.2: {} + semver@7.6.3: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -11560,6 +13183,32 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11579,6 +13228,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -11784,6 +13437,8 @@ snapshots: tabbable@6.2.0: {} + tailwind-merge@2.6.0: {} + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.7.2)): dependencies: '@alloc/quick-lru': 5.2.0 @@ -11898,6 +13553,10 @@ snapshots: dependencies: punycode: 2.3.1 + transliteration@2.3.5: + dependencies: + yargs: 17.7.2 + traverse@0.3.9: {} tree-kill@1.2.2: {} @@ -12234,6 +13893,10 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -12298,6 +13961,9 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: + optional: true + yaml@2.4.5: {} yargs-parser@13.1.2: diff --git a/web-dist/error.html b/web-dist/error.html new file mode 100644 index 0000000..ecfd539 --- /dev/null +++ b/web-dist/error.html @@ -0,0 +1 @@ +

error

\ No newline at end of file diff --git a/web-dist/index.html b/web-dist/index.html new file mode 100644 index 0000000..48b59bb --- /dev/null +++ b/web-dist/index.html @@ -0,0 +1 @@ +

test1

\ No newline at end of file