refactor(auth): remove themes module from mana-core-auth

Remove unused themes API from auth service:
- Delete themes.schema.ts database schema
- Delete themes.controller.ts, themes.service.ts, themes.module.ts
- Remove ThemesModule from app.module.ts imports
- Remove themes schema export from db/schema/index.ts

Custom themes are no longer supported - the built-in theme variants
provide sufficient customization options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-12 02:35:18 +01:00
parent 12ba2cf824
commit e473a026ee
7 changed files with 0 additions and 1188 deletions

View file

@ -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: [
{

View file

@ -4,4 +4,3 @@ export * from './feedback.schema';
export * from './organizations.schema';
export * from './referrals.schema';
export * from './tags.schema';
export * from './themes.schema';

View file

@ -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;

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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 {}

View file

@ -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<string>('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<PaginatedCommunityThemesDto> {
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<number>`count(*)` })
.from(communityThemes)
.where(and(...conditions));
// If user is authenticated, get their favorites, downloads, and ratings
let userFavorites = new Set<string>();
let userDownloads = new Set<string>();
let userRatingsMap = new Map<string, number>();
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<CommunityThemeResponseDto> {
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,
}));
}
}