diff --git a/services/mana-core-auth/src/app.module.ts b/services/mana-core-auth/src/app.module.ts index 4fcd1bcb5..0cb0ce214 100644 --- a/services/mana-core-auth/src/app.module.ts +++ b/services/mana-core-auth/src/app.module.ts @@ -9,7 +9,6 @@ import { FeedbackModule } from './feedback/feedback.module'; import { ReferralsModule } from './referrals/referrals.module'; import { SettingsModule } from './settings/settings.module'; import { TagsModule } from './tags/tags.module'; -import { ThemesModule } from './themes/themes.module'; import { AiModule } from './ai/ai.module'; import { HealthModule } from './health/health.module'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @@ -34,7 +33,6 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; ReferralsModule, SettingsModule, TagsModule, - ThemesModule, ], providers: [ { diff --git a/services/mana-core-auth/src/db/schema/index.ts b/services/mana-core-auth/src/db/schema/index.ts index a9c6b85ab..72a7970f2 100644 --- a/services/mana-core-auth/src/db/schema/index.ts +++ b/services/mana-core-auth/src/db/schema/index.ts @@ -4,4 +4,3 @@ export * from './feedback.schema'; export * from './organizations.schema'; export * from './referrals.schema'; export * from './tags.schema'; -export * from './themes.schema'; diff --git a/services/mana-core-auth/src/db/schema/themes.schema.ts b/services/mana-core-auth/src/db/schema/themes.schema.ts deleted file mode 100644 index 723cf4712..000000000 --- a/services/mana-core-auth/src/db/schema/themes.schema.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { uuid, text, timestamp, boolean, jsonb, integer, index } from 'drizzle-orm/pg-core'; -import { users, authSchema } from './auth.schema'; - -/** - * Custom Themes - Private themes created by users - */ -export const customThemes = authSchema.table( - 'custom_themes', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), - - // Theme metadata - name: text('name').notNull(), - description: text('description'), - emoji: text('emoji').default('🎨'), - icon: text('icon').default('palette'), - - // Colors (JSONB - ThemeColors interface) - lightColors: jsonb('light_colors').notNull(), - darkColors: jsonb('dark_colors').notNull(), - - // Base variant this theme was derived from (optional) - baseVariant: text('base_variant'), // 'lume' | 'nature' | 'stone' | 'ocean' | null - - // Publishing status - isPublished: boolean('is_published').default(false).notNull(), - - // Timestamps - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - userIdx: index('custom_themes_user_idx').on(table.userId), - }) -); - -/** - * Community Themes - Public themes shared with all users - */ -export const communityThemes = authSchema.table( - 'community_themes', - { - id: uuid('id').primaryKey().defaultRandom(), - authorId: text('author_id').references(() => users.id, { onDelete: 'set null' }), - - // Theme metadata - name: text('name').notNull(), - description: text('description'), - emoji: text('emoji').default('🎨'), - icon: text('icon').default('palette'), - - // Colors (JSONB - ThemeColors interface) - lightColors: jsonb('light_colors').notNull(), - darkColors: jsonb('dark_colors').notNull(), - - // Base variant (for compatibility preview) - baseVariant: text('base_variant'), - - // Statistics - downloadCount: integer('download_count').default(0).notNull(), - ratingSum: integer('rating_sum').default(0).notNull(), - ratingCount: integer('rating_count').default(0).notNull(), - - // Moderation status: pending -> approved (or rejected), featured for promoted themes - status: text('status').default('pending').notNull(), // 'pending' | 'approved' | 'rejected' | 'featured' - isFeatured: boolean('is_featured').default(false).notNull(), - - // Tags for search/filtering - tags: jsonb('tags').default([]).notNull(), // string[] - - // Timestamps - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - publishedAt: timestamp('published_at', { withTimezone: true }), - }, - (table) => ({ - authorIdx: index('community_themes_author_idx').on(table.authorId), - statusIdx: index('community_themes_status_idx').on(table.status), - downloadIdx: index('community_themes_download_idx').on(table.downloadCount), - featuredIdx: index('community_themes_featured_idx').on(table.isFeatured), - }) -); - -/** - * User Theme Favorites - Users can favorite community themes - */ -export const userThemeFavorites = authSchema.table( - 'user_theme_favorites', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), - communityThemeId: uuid('community_theme_id') - .references(() => communityThemes.id, { onDelete: 'cascade' }) - .notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - userThemeIdx: index('user_theme_favorites_user_theme_idx').on( - table.userId, - table.communityThemeId - ), - }) -); - -/** - * User Theme Downloads - Track which users downloaded which themes - */ -export const userThemeDownloads = authSchema.table( - 'user_theme_downloads', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), - communityThemeId: uuid('community_theme_id') - .references(() => communityThemes.id, { onDelete: 'cascade' }) - .notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - userDownloadIdx: index('user_theme_downloads_user_theme_idx').on( - table.userId, - table.communityThemeId - ), - }) -); - -/** - * Theme Ratings - Users can rate community themes (1-5 stars) - */ -export const themeRatings = authSchema.table( - 'theme_ratings', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id') - .references(() => users.id, { onDelete: 'cascade' }) - .notNull(), - communityThemeId: uuid('community_theme_id') - .references(() => communityThemes.id, { onDelete: 'cascade' }) - .notNull(), - rating: integer('rating').notNull(), // 1-5 - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => ({ - userRatingIdx: index('theme_ratings_user_theme_idx').on(table.userId, table.communityThemeId), - }) -); - -// Type exports for use in services -export type CustomTheme = typeof customThemes.$inferSelect; -export type NewCustomTheme = typeof customThemes.$inferInsert; -export type CommunityTheme = typeof communityThemes.$inferSelect; -export type NewCommunityTheme = typeof communityThemes.$inferInsert; -export type UserThemeFavorite = typeof userThemeFavorites.$inferSelect; -export type UserThemeDownload = typeof userThemeDownloads.$inferSelect; -export type ThemeRating = typeof themeRatings.$inferSelect; diff --git a/services/mana-core-auth/src/themes/dto/index.ts b/services/mana-core-auth/src/themes/dto/index.ts deleted file mode 100644 index 6755249c4..000000000 --- a/services/mana-core-auth/src/themes/dto/index.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { - IsString, - IsOptional, - IsObject, - IsBoolean, - IsArray, - IsInt, - Min, - Max, - IsEnum, - IsUUID, -} from 'class-validator'; -import { Type } from 'class-transformer'; - -/** - * ThemeColors structure matching the frontend ThemeColors interface - */ -export class ThemeColorsDto { - @IsString() - primary: string; - - @IsString() - @IsOptional() - primaryForeground?: string; - - @IsString() - background: string; - - @IsString() - foreground: string; - - @IsString() - surface: string; - - @IsString() - @IsOptional() - surfaceHover?: string; - - @IsString() - @IsOptional() - surfaceElevated?: string; - - @IsString() - @IsOptional() - muted?: string; - - @IsString() - @IsOptional() - mutedForeground?: string; - - @IsString() - @IsOptional() - border?: string; - - @IsString() - @IsOptional() - borderStrong?: string; - - @IsString() - @IsOptional() - secondary?: string; - - @IsString() - @IsOptional() - secondaryForeground?: string; - - @IsString() - @IsOptional() - input?: string; - - @IsString() - @IsOptional() - ring?: string; - - @IsString() - error: string; - - @IsString() - success: string; - - @IsString() - warning: string; -} - -/** - * Create a new custom theme - */ -export class CreateCustomThemeDto { - @IsString() - name: string; - - @IsString() - @IsOptional() - description?: string; - - @IsString() - @IsOptional() - emoji?: string; - - @IsString() - @IsOptional() - icon?: string; - - @IsObject() - @Type(() => ThemeColorsDto) - lightColors: ThemeColorsDto; - - @IsObject() - @Type(() => ThemeColorsDto) - darkColors: ThemeColorsDto; - - @IsString() - @IsOptional() - @IsEnum(['lume', 'nature', 'stone', 'ocean']) - baseVariant?: 'lume' | 'nature' | 'stone' | 'ocean'; -} - -/** - * Update an existing custom theme - */ -export class UpdateCustomThemeDto { - @IsString() - @IsOptional() - name?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsString() - @IsOptional() - emoji?: string; - - @IsString() - @IsOptional() - icon?: string; - - @IsObject() - @IsOptional() - @Type(() => ThemeColorsDto) - lightColors?: ThemeColorsDto; - - @IsObject() - @IsOptional() - @Type(() => ThemeColorsDto) - darkColors?: ThemeColorsDto; - - @IsString() - @IsOptional() - @IsEnum(['lume', 'nature', 'stone', 'ocean']) - baseVariant?: 'lume' | 'nature' | 'stone' | 'ocean'; -} - -/** - * Publish a custom theme to the community - */ -export class PublishThemeDto { - @IsArray() - @IsString({ each: true }) - @IsOptional() - tags?: string[]; - - @IsString() - @IsOptional() - description?: string; -} - -/** - * Query parameters for browsing community themes - */ -export class ThemeQueryDto { - @IsInt() - @Min(1) - @IsOptional() - @Type(() => Number) - page?: number = 1; - - @IsInt() - @Min(1) - @Max(100) - @IsOptional() - @Type(() => Number) - limit?: number = 20; - - @IsString() - @IsOptional() - @IsEnum(['popular', 'recent', 'rating', 'downloads']) - sort?: 'popular' | 'recent' | 'rating' | 'downloads' = 'popular'; - - @IsString() - @IsOptional() - search?: string; - - @IsArray() - @IsString({ each: true }) - @IsOptional() - tags?: string[]; - - @IsString() - @IsOptional() - authorId?: string; - - @IsBoolean() - @IsOptional() - @Type(() => Boolean) - featuredOnly?: boolean; -} - -/** - * Rate a community theme - */ -export class RateThemeDto { - @IsInt() - @Min(1) - @Max(5) - rating: number; -} - -/** - * Response for a custom theme - */ -export class CustomThemeResponseDto { - id: string; - userId: string; - name: string; - description?: string; - emoji: string; - icon: string; - lightColors: ThemeColorsDto; - darkColors: ThemeColorsDto; - baseVariant?: string; - isPublished: boolean; - createdAt: Date; - updatedAt: Date; -} - -/** - * Response for a community theme - */ -export class CommunityThemeResponseDto { - id: string; - authorId?: string; - authorName?: string; - name: string; - description?: string; - emoji: string; - icon: string; - lightColors: ThemeColorsDto; - darkColors: ThemeColorsDto; - baseVariant?: string; - downloadCount: number; - averageRating: number; - ratingCount: number; - status: string; - isFeatured: boolean; - tags: string[]; - createdAt: Date; - publishedAt?: Date; - // User-specific fields (when authenticated) - isFavorited?: boolean; - isDownloaded?: boolean; - userRating?: number; -} - -/** - * Paginated response for community themes - */ -export class PaginatedCommunityThemesDto { - themes: CommunityThemeResponseDto[]; - total: number; - page: number; - limit: number; - totalPages: number; -} diff --git a/services/mana-core-auth/src/themes/themes.controller.ts b/services/mana-core-auth/src/themes/themes.controller.ts deleted file mode 100644 index db542bbb9..000000000 --- a/services/mana-core-auth/src/themes/themes.controller.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - Controller, - Get, - Post, - Patch, - Delete, - Body, - Param, - Query, - UseGuards, - HttpCode, - HttpStatus, -} from '@nestjs/common'; -import { ThemesService } from './themes.service'; -import { - CreateCustomThemeDto, - UpdateCustomThemeDto, - PublishThemeDto, - ThemeQueryDto, - RateThemeDto, -} from './dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; -import { CurrentUser } from '../common/decorators/current-user.decorator'; -import type { CurrentUserData } from '../common/decorators/current-user.decorator'; - -@Controller() -export class ThemesController { - constructor(private readonly themesService: ThemesService) {} - - // ==================== Custom Themes ==================== - - /** - * Create a new custom theme - */ - @Post('themes') - @UseGuards(JwtAuthGuard) - async createCustomTheme(@CurrentUser() user: CurrentUserData, @Body() dto: CreateCustomThemeDto) { - return this.themesService.createCustomTheme(user.userId, dto); - } - - /** - * Get all custom themes for the current user - */ - @Get('themes') - @UseGuards(JwtAuthGuard) - async getCustomThemes(@CurrentUser() user: CurrentUserData) { - return this.themesService.getCustomThemes(user.userId); - } - - /** - * Get a specific custom theme - */ - @Get('themes/:id') - @UseGuards(JwtAuthGuard) - async getCustomTheme(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { - return this.themesService.getCustomTheme(user.userId, id); - } - - /** - * Update a custom theme - */ - @Patch('themes/:id') - @UseGuards(JwtAuthGuard) - async updateCustomTheme( - @CurrentUser() user: CurrentUserData, - @Param('id') id: string, - @Body() dto: UpdateCustomThemeDto - ) { - return this.themesService.updateCustomTheme(user.userId, id, dto); - } - - /** - * Delete a custom theme - */ - @Delete('themes/:id') - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.NO_CONTENT) - async deleteCustomTheme(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { - await this.themesService.deleteCustomTheme(user.userId, id); - } - - /** - * Publish a custom theme to the community - */ - @Post('themes/:id/publish') - @UseGuards(JwtAuthGuard) - async publishTheme( - @CurrentUser() user: CurrentUserData, - @Param('id') id: string, - @Body() dto: PublishThemeDto - ) { - return this.themesService.publishTheme(user.userId, id, dto); - } - - // ==================== Community Themes ==================== - - /** - * Browse community themes with filtering, sorting, and pagination - */ - @Get('community-themes') - async getCommunityThemes(@Query() query: ThemeQueryDto, @CurrentUser() user?: CurrentUserData) { - return this.themesService.getCommunityThemes(query, user?.userId); - } - - /** - * Get user's favorite community themes - */ - @Get('community-themes/favorites') - @UseGuards(JwtAuthGuard) - async getFavorites(@CurrentUser() user: CurrentUserData) { - return this.themesService.getFavorites(user.userId); - } - - /** - * Get user's downloaded community themes - */ - @Get('community-themes/downloaded') - @UseGuards(JwtAuthGuard) - async getDownloadedThemes(@CurrentUser() user: CurrentUserData) { - return this.themesService.getDownloadedThemes(user.userId); - } - - /** - * Get a specific community theme - */ - @Get('community-themes/:id') - async getCommunityTheme(@Param('id') id: string, @CurrentUser() user?: CurrentUserData) { - return this.themesService.getCommunityTheme(id, user?.userId); - } - - /** - * Download/install a community theme - */ - @Post('community-themes/:id/download') - @UseGuards(JwtAuthGuard) - async downloadTheme(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { - return this.themesService.downloadTheme(user.userId, id); - } - - /** - * Rate a community theme (1-5 stars) - */ - @Post('community-themes/:id/rate') - @UseGuards(JwtAuthGuard) - async rateTheme( - @CurrentUser() user: CurrentUserData, - @Param('id') id: string, - @Body() dto: RateThemeDto - ) { - return this.themesService.rateTheme(user.userId, id, dto.rating); - } - - /** - * Toggle favorite status for a community theme - */ - @Post('community-themes/:id/favorite') - @UseGuards(JwtAuthGuard) - async toggleFavorite(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { - return this.themesService.toggleFavorite(user.userId, id); - } -} diff --git a/services/mana-core-auth/src/themes/themes.module.ts b/services/mana-core-auth/src/themes/themes.module.ts deleted file mode 100644 index 51dbcd09c..000000000 --- a/services/mana-core-auth/src/themes/themes.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ThemesController } from './themes.controller'; -import { ThemesService } from './themes.service'; - -@Module({ - controllers: [ThemesController], - providers: [ThemesService], - exports: [ThemesService], -}) -export class ThemesModule {} diff --git a/services/mana-core-auth/src/themes/themes.service.ts b/services/mana-core-auth/src/themes/themes.service.ts deleted file mode 100644 index 9723a4578..000000000 --- a/services/mana-core-auth/src/themes/themes.service.ts +++ /dev/null @@ -1,578 +0,0 @@ -import { - Injectable, - NotFoundException, - ForbiddenException, - ConflictException, -} from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { eq, and, desc, ilike, inArray, sql } from 'drizzle-orm'; -import { getDb } from '../db/connection'; -import { - customThemes, - communityThemes, - userThemeFavorites, - userThemeDownloads, - themeRatings, - users, -} from '../db/schema'; -import { - CreateCustomThemeDto, - UpdateCustomThemeDto, - PublishThemeDto, - ThemeQueryDto, - CommunityThemeResponseDto, - PaginatedCommunityThemesDto, -} from './dto'; - -@Injectable() -export class ThemesService { - constructor(private configService: ConfigService) {} - - private getDb() { - const databaseUrl = this.configService.get('database.url'); - return getDb(databaseUrl!); - } - - // ==================== Custom Themes ==================== - - /** - * Create a new custom theme for a user - */ - async createCustomTheme(userId: string, dto: CreateCustomThemeDto) { - const [theme] = await this.getDb() - .insert(customThemes) - .values({ - userId, - name: dto.name, - description: dto.description, - emoji: dto.emoji || '🎨', - icon: dto.icon || 'palette', - lightColors: dto.lightColors, - darkColors: dto.darkColors, - baseVariant: dto.baseVariant, - }) - .returning(); - - return theme; - } - - /** - * Get all custom themes for a user - */ - async getCustomThemes(userId: string) { - return this.getDb() - .select() - .from(customThemes) - .where(eq(customThemes.userId, userId)) - .orderBy(desc(customThemes.updatedAt)); - } - - /** - * Get a specific custom theme - */ - async getCustomTheme(userId: string, themeId: string) { - const [theme] = await this.getDb() - .select() - .from(customThemes) - .where(and(eq(customThemes.id, themeId), eq(customThemes.userId, userId))); - - if (!theme) { - throw new NotFoundException('Theme not found'); - } - - return theme; - } - - /** - * Update a custom theme - */ - async updateCustomTheme(userId: string, themeId: string, dto: UpdateCustomThemeDto) { - // Verify ownership - await this.getCustomTheme(userId, themeId); - - const [updated] = await this.getDb() - .update(customThemes) - .set({ - ...dto, - updatedAt: new Date(), - }) - .where(and(eq(customThemes.id, themeId), eq(customThemes.userId, userId))) - .returning(); - - return updated; - } - - /** - * Delete a custom theme - */ - async deleteCustomTheme(userId: string, themeId: string) { - // Verify ownership - await this.getCustomTheme(userId, themeId); - - await this.getDb() - .delete(customThemes) - .where(and(eq(customThemes.id, themeId), eq(customThemes.userId, userId))); - - return { success: true }; - } - - /** - * Publish a custom theme to the community - */ - async publishTheme(userId: string, themeId: string, dto: PublishThemeDto) { - const theme = await this.getCustomTheme(userId, themeId); - - if (theme.isPublished) { - throw new ConflictException('Theme is already published'); - } - - // Create community theme entry (pending approval) - const [communityTheme] = await this.getDb() - .insert(communityThemes) - .values({ - authorId: userId, - name: theme.name, - description: dto.description || theme.description, - emoji: theme.emoji, - icon: theme.icon, - lightColors: theme.lightColors, - darkColors: theme.darkColors, - baseVariant: theme.baseVariant, - tags: dto.tags || [], - status: 'pending', - }) - .returning(); - - // Mark custom theme as published - await this.getDb() - .update(customThemes) - .set({ isPublished: true, updatedAt: new Date() }) - .where(eq(customThemes.id, themeId)); - - return communityTheme; - } - - // ==================== Community Themes ==================== - - /** - * Browse community themes with filtering and pagination - */ - async getCommunityThemes( - query: ThemeQueryDto, - userId?: string - ): Promise { - const { page = 1, limit = 20, sort = 'popular', search, tags, authorId, featuredOnly } = query; - const offset = (page - 1) * limit; - - // Build where conditions - const conditions = [eq(communityThemes.status, 'approved')]; - - if (featuredOnly) { - conditions.push(eq(communityThemes.isFeatured, true)); - } - - if (authorId) { - conditions.push(eq(communityThemes.authorId, authorId)); - } - - if (search) { - conditions.push(ilike(communityThemes.name, `%${search}%`)); - } - - // Build order by - let orderBy; - switch (sort) { - case 'recent': - orderBy = desc(communityThemes.publishedAt); - break; - case 'rating': - orderBy = desc( - sql`CASE WHEN ${communityThemes.ratingCount} > 0 THEN ${communityThemes.ratingSum}::float / ${communityThemes.ratingCount} ELSE 0 END` - ); - break; - case 'downloads': - orderBy = desc(communityThemes.downloadCount); - break; - case 'popular': - default: - // Popular = combination of downloads and rating - orderBy = desc( - sql`${communityThemes.downloadCount} + (CASE WHEN ${communityThemes.ratingCount} > 0 THEN ${communityThemes.ratingSum}::float / ${communityThemes.ratingCount} * 10 ELSE 0 END)` - ); - break; - } - - // Get themes with author info - const themesQuery = this.getDb() - .select({ - theme: communityThemes, - authorName: users.name, - }) - .from(communityThemes) - .leftJoin(users, eq(communityThemes.authorId, users.id)) - .where(and(...conditions)) - .orderBy(orderBy) - .limit(limit) - .offset(offset); - - const themes = await themesQuery; - - // Get total count - const [{ count }] = await this.getDb() - .select({ count: sql`count(*)` }) - .from(communityThemes) - .where(and(...conditions)); - - // If user is authenticated, get their favorites, downloads, and ratings - let userFavorites = new Set(); - let userDownloads = new Set(); - let userRatingsMap = new Map(); - - if (userId) { - const themeIds = themes.map((t) => t.theme.id); - - if (themeIds.length > 0) { - const favorites = await this.getDb() - .select() - .from(userThemeFavorites) - .where( - and( - eq(userThemeFavorites.userId, userId), - inArray(userThemeFavorites.communityThemeId, themeIds) - ) - ); - userFavorites = new Set(favorites.map((f) => f.communityThemeId)); - - const downloads = await this.getDb() - .select() - .from(userThemeDownloads) - .where( - and( - eq(userThemeDownloads.userId, userId), - inArray(userThemeDownloads.communityThemeId, themeIds) - ) - ); - userDownloads = new Set(downloads.map((d) => d.communityThemeId)); - - const ratings = await this.getDb() - .select() - .from(themeRatings) - .where( - and(eq(themeRatings.userId, userId), inArray(themeRatings.communityThemeId, themeIds)) - ); - ratings.forEach((r) => userRatingsMap.set(r.communityThemeId, r.rating)); - } - } - - // Transform to response DTOs - const responseThemes: CommunityThemeResponseDto[] = themes.map(({ theme, authorName }) => ({ - id: theme.id, - authorId: theme.authorId ?? undefined, - authorName: authorName || undefined, - name: theme.name, - description: theme.description || undefined, - emoji: theme.emoji || '🎨', - icon: theme.icon || 'palette', - lightColors: theme.lightColors as any, - darkColors: theme.darkColors as any, - baseVariant: theme.baseVariant || undefined, - downloadCount: theme.downloadCount, - averageRating: theme.ratingCount > 0 ? theme.ratingSum / theme.ratingCount : 0, - ratingCount: theme.ratingCount, - status: theme.status, - isFeatured: theme.isFeatured, - tags: (theme.tags as string[]) || [], - createdAt: theme.createdAt, - publishedAt: theme.publishedAt || undefined, - isFavorited: userFavorites.has(theme.id), - isDownloaded: userDownloads.has(theme.id), - userRating: userRatingsMap.get(theme.id), - })); - - return { - themes: responseThemes, - total: Number(count), - page, - limit, - totalPages: Math.ceil(Number(count) / limit), - }; - } - - /** - * Get a specific community theme - */ - async getCommunityTheme(themeId: string, userId?: string): Promise { - const [result] = await this.getDb() - .select({ - theme: communityThemes, - authorName: users.name, - }) - .from(communityThemes) - .leftJoin(users, eq(communityThemes.authorId, users.id)) - .where(eq(communityThemes.id, themeId)); - - if (!result) { - throw new NotFoundException('Theme not found'); - } - - const { theme, authorName } = result; - - // Get user-specific data if authenticated - let isFavorited = false; - let isDownloaded = false; - let userRating: number | undefined; - - if (userId) { - const [favorite] = await this.getDb() - .select() - .from(userThemeFavorites) - .where( - and( - eq(userThemeFavorites.userId, userId), - eq(userThemeFavorites.communityThemeId, themeId) - ) - ); - isFavorited = !!favorite; - - const [download] = await this.getDb() - .select() - .from(userThemeDownloads) - .where( - and( - eq(userThemeDownloads.userId, userId), - eq(userThemeDownloads.communityThemeId, themeId) - ) - ); - isDownloaded = !!download; - - const [rating] = await this.getDb() - .select() - .from(themeRatings) - .where(and(eq(themeRatings.userId, userId), eq(themeRatings.communityThemeId, themeId))); - userRating = rating?.rating; - } - - return { - id: theme.id, - authorId: theme.authorId ?? undefined, - authorName: authorName || undefined, - name: theme.name, - description: theme.description || undefined, - emoji: theme.emoji || '🎨', - icon: theme.icon || 'palette', - lightColors: theme.lightColors as any, - darkColors: theme.darkColors as any, - baseVariant: theme.baseVariant || undefined, - downloadCount: theme.downloadCount, - averageRating: theme.ratingCount > 0 ? theme.ratingSum / theme.ratingCount : 0, - ratingCount: theme.ratingCount, - status: theme.status, - isFeatured: theme.isFeatured, - tags: (theme.tags as string[]) || [], - createdAt: theme.createdAt, - publishedAt: theme.publishedAt || undefined, - isFavorited, - isDownloaded, - userRating, - }; - } - - /** - * Download/install a community theme - */ - async downloadTheme(userId: string, themeId: string) { - const theme = await this.getCommunityTheme(themeId); - - if (theme.status !== 'approved' && theme.status !== 'featured') { - throw new ForbiddenException('Theme is not available for download'); - } - - // Check if already downloaded - const [existing] = await this.getDb() - .select() - .from(userThemeDownloads) - .where( - and(eq(userThemeDownloads.userId, userId), eq(userThemeDownloads.communityThemeId, themeId)) - ); - - if (!existing) { - // Record download - await this.getDb().insert(userThemeDownloads).values({ - userId, - communityThemeId: themeId, - }); - - // Increment download count - await this.getDb() - .update(communityThemes) - .set({ - downloadCount: sql`${communityThemes.downloadCount} + 1`, - }) - .where(eq(communityThemes.id, themeId)); - } - - // Return the theme data for the user to apply - return theme; - } - - /** - * Rate a community theme - */ - async rateTheme(userId: string, themeId: string, rating: number) { - const theme = await this.getCommunityTheme(themeId); - - // Check for existing rating - const [existingRating] = await this.getDb() - .select() - .from(themeRatings) - .where(and(eq(themeRatings.userId, userId), eq(themeRatings.communityThemeId, themeId))); - - if (existingRating) { - // Update existing rating - const ratingDiff = rating - existingRating.rating; - - await this.getDb() - .update(themeRatings) - .set({ rating, updatedAt: new Date() }) - .where(eq(themeRatings.id, existingRating.id)); - - await this.getDb() - .update(communityThemes) - .set({ - ratingSum: sql`${communityThemes.ratingSum} + ${ratingDiff}`, - }) - .where(eq(communityThemes.id, themeId)); - } else { - // Create new rating - await this.getDb().insert(themeRatings).values({ - userId, - communityThemeId: themeId, - rating, - }); - - await this.getDb() - .update(communityThemes) - .set({ - ratingSum: sql`${communityThemes.ratingSum} + ${rating}`, - ratingCount: sql`${communityThemes.ratingCount} + 1`, - }) - .where(eq(communityThemes.id, themeId)); - } - - // Get updated theme to return new stats - const [updatedTheme] = await this.getDb() - .select() - .from(communityThemes) - .where(eq(communityThemes.id, themeId)); - - return { - averageRating: - updatedTheme.ratingCount > 0 ? updatedTheme.ratingSum / updatedTheme.ratingCount : 0, - ratingCount: updatedTheme.ratingCount, - }; - } - - /** - * Toggle favorite status for a community theme - */ - async toggleFavorite(userId: string, themeId: string) { - // Verify theme exists - await this.getCommunityTheme(themeId); - - const [existing] = await this.getDb() - .select() - .from(userThemeFavorites) - .where( - and(eq(userThemeFavorites.userId, userId), eq(userThemeFavorites.communityThemeId, themeId)) - ); - - if (existing) { - // Remove favorite - await this.getDb().delete(userThemeFavorites).where(eq(userThemeFavorites.id, existing.id)); - return { isFavorited: false }; - } else { - // Add favorite - await this.getDb().insert(userThemeFavorites).values({ - userId, - communityThemeId: themeId, - }); - return { isFavorited: true }; - } - } - - /** - * Get user's favorite themes - */ - async getFavorites(userId: string) { - const favorites = await this.getDb() - .select({ - theme: communityThemes, - authorName: users.name, - }) - .from(userThemeFavorites) - .innerJoin(communityThemes, eq(userThemeFavorites.communityThemeId, communityThemes.id)) - .leftJoin(users, eq(communityThemes.authorId, users.id)) - .where(eq(userThemeFavorites.userId, userId)) - .orderBy(desc(userThemeFavorites.createdAt)); - - return favorites.map(({ theme, authorName }) => ({ - id: theme.id, - authorId: theme.authorId, - authorName: authorName || undefined, - name: theme.name, - description: theme.description || undefined, - emoji: theme.emoji || '🎨', - icon: theme.icon || 'palette', - lightColors: theme.lightColors, - darkColors: theme.darkColors, - baseVariant: theme.baseVariant || undefined, - downloadCount: theme.downloadCount, - averageRating: theme.ratingCount > 0 ? theme.ratingSum / theme.ratingCount : 0, - ratingCount: theme.ratingCount, - status: theme.status, - isFeatured: theme.isFeatured, - tags: (theme.tags as string[]) || [], - createdAt: theme.createdAt, - publishedAt: theme.publishedAt || undefined, - isFavorited: true, - })); - } - - /** - * Get user's downloaded themes - */ - async getDownloadedThemes(userId: string) { - const downloads = await this.getDb() - .select({ - theme: communityThemes, - authorName: users.name, - }) - .from(userThemeDownloads) - .innerJoin(communityThemes, eq(userThemeDownloads.communityThemeId, communityThemes.id)) - .leftJoin(users, eq(communityThemes.authorId, users.id)) - .where(eq(userThemeDownloads.userId, userId)) - .orderBy(desc(userThemeDownloads.createdAt)); - - return downloads.map(({ theme, authorName }) => ({ - id: theme.id, - authorId: theme.authorId, - authorName: authorName || undefined, - name: theme.name, - description: theme.description || undefined, - emoji: theme.emoji || '🎨', - icon: theme.icon || 'palette', - lightColors: theme.lightColors, - darkColors: theme.darkColors, - baseVariant: theme.baseVariant || undefined, - downloadCount: theme.downloadCount, - averageRating: theme.ratingCount > 0 ? theme.ratingSum / theme.ratingCount : 0, - ratingCount: theme.ratingCount, - status: theme.status, - isFeatured: theme.isFeatured, - tags: (theme.tags as string[]) || [], - createdAt: theme.createdAt, - publishedAt: theme.publishedAt || undefined, - isDownloaded: true, - })); - } -}