Add complete CKE Project implementation with news management system

This commit is contained in:
albivkt
2025-07-13 01:34:11 +03:00
parent c9317555ca
commit a84810c6b9
32 changed files with 8901 additions and 811 deletions

605
lib/database.ts Normal file
View File

@ -0,0 +1,605 @@
import { PrismaClient } from '@prisma/client';
declare global {
var prisma: PrismaClient | undefined;
}
const prisma = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma;
}
export default prisma;
// Типы для работы с новостями
export interface NewsItem {
id: string;
title: string;
slug: string;
summary: string;
content: string;
category: string;
imageUrl?: string;
featured: boolean;
published: boolean;
publishedAt: Date;
createdAt: Date;
updatedAt: Date;
authorId?: string;
author?: {
id: string;
name?: string;
username: string;
email: string;
};
views: number;
likes: number;
tags: string[];
}
export interface NewsCategory {
id: string;
name: string;
slug: string;
description?: string;
color: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateNewsData {
title: string;
slug: string;
summary: string;
content: string;
category: string;
imageUrl?: string;
featured?: boolean;
published?: boolean;
publishedAt?: Date;
authorId?: string;
tags?: string[];
}
export interface UpdateNewsData {
title?: string;
slug?: string;
summary?: string;
content?: string;
category?: string;
imageUrl?: string;
featured?: boolean;
published?: boolean;
publishedAt?: Date;
tags?: string[];
}
export interface NewsFilters {
category?: string;
search?: string;
featured?: boolean;
published?: boolean;
authorId?: string;
tags?: string[];
}
export interface NewsPagination {
page: number;
limit: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface NewsListResponse {
news: NewsItem[];
total: number;
page: number;
limit: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
}
// Утилиты для работы с новостями
export class NewsService {
static async getNews(id: string): Promise<NewsItem | null> {
return await prisma.news.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
}) as NewsItem | null;
}
static async getNewsBySlug(slug: string): Promise<NewsItem | null> {
return await prisma.news.findUnique({
where: { slug },
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
}) as NewsItem | null;
}
static async getNewsList(
filters: NewsFilters = {},
pagination: NewsPagination = { page: 1, limit: 10 }
): Promise<NewsListResponse> {
const {
category,
search,
featured,
published = true,
authorId,
tags
} = filters;
const {
page = 1,
limit = 10,
sortBy = 'publishedAt',
sortOrder = 'desc'
} = pagination;
const skip = (page - 1) * limit;
const where: any = {};
if (published !== undefined) {
where.published = published;
}
if (category && category !== 'all') {
where.category = category;
}
if (featured !== undefined) {
where.featured = featured;
}
if (authorId) {
where.authorId = authorId;
}
if (tags && tags.length > 0) {
where.tags = {
hasSome: tags
};
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ summary: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
{ tags: { hasSome: [search] } }
];
}
const orderBy: any = {};
orderBy[sortBy] = sortOrder;
const [news, total] = await Promise.all([
prisma.news.findMany({
where,
skip,
take: limit,
orderBy,
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
}),
prisma.news.count({ where })
]);
const totalPages = Math.ceil(total / limit);
return {
news,
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
};
}
static async createNews(data: CreateNewsData): Promise<NewsItem> {
// Преобразуем теги из массива в строку для сохранения в БД
const tagsString = data.tags ? data.tags.join(',') : '';
const result = await prisma.news.create({
data: {
...data,
tags: tagsString,
publishedAt: data.publishedAt || new Date()
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
// Преобразуем теги обратно в массив для возврата
return {
...result,
tags: result.tags ? result.tags.split(',').filter(tag => tag.trim()) : []
};
}
static async updateNews(id: string, data: UpdateNewsData): Promise<NewsItem> {
// Преобразуем теги из массива в строку для сохранения в БД
const updateData: any = { ...data };
if (data.tags) {
updateData.tags = data.tags.join(',');
}
return await prisma.news.update({
where: { id },
data: updateData,
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
}) as NewsItem;
}
static async deleteNews(id: string): Promise<void> {
await prisma.news.delete({
where: { id }
});
}
static async incrementViews(id: string): Promise<NewsItem> {
return await prisma.news.update({
where: { id },
data: {
views: { increment: 1 }
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async incrementLikes(id: string): Promise<NewsItem> {
return await prisma.news.update({
where: { id },
data: {
likes: { increment: 1 }
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async togglePublished(id: string): Promise<NewsItem> {
const news = await prisma.news.findUnique({ where: { id } });
if (!news) throw new Error('News not found');
return await prisma.news.update({
where: { id },
data: {
published: !news.published
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async toggleFeatured(id: string): Promise<NewsItem> {
const news = await prisma.news.findUnique({ where: { id } });
if (!news) throw new Error('News not found');
return await prisma.news.update({
where: { id },
data: {
featured: !news.featured
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async getRelatedNews(newsId: string, category: string, limit = 3): Promise<NewsItem[]> {
return await prisma.news.findMany({
where: {
AND: [
{ id: { not: newsId } },
{ category },
{ published: true }
]
},
take: limit,
orderBy: {
publishedAt: 'desc'
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async getPopularNews(limit = 5): Promise<NewsItem[]> {
return await prisma.news.findMany({
where: { published: true },
take: limit,
orderBy: [
{ views: 'desc' },
{ likes: 'desc' },
{ publishedAt: 'desc' }
],
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async getFeaturedNews(limit = 3): Promise<NewsItem[]> {
return await prisma.news.findMany({
where: {
featured: true,
published: true
},
take: limit,
orderBy: {
publishedAt: 'desc'
},
include: {
author: {
select: {
id: true,
name: true,
username: true,
email: true
}
}
}
});
}
static async getNewsStats(): Promise<{
total: number;
published: number;
draft: number;
featured: number;
totalViews: number;
totalLikes: number;
}> {
const [
total,
published,
draft,
featured,
viewsResult,
likesResult
] = await Promise.all([
prisma.news.count(),
prisma.news.count({ where: { published: true } }),
prisma.news.count({ where: { published: false } }),
prisma.news.count({ where: { featured: true } }),
prisma.news.aggregate({
_sum: { views: true }
}),
prisma.news.aggregate({
_sum: { likes: true }
})
]);
return {
total,
published,
draft,
featured,
totalViews: viewsResult._sum.views || 0,
totalLikes: likesResult._sum.likes || 0
};
}
}
// Утилиты для работы с категориями
export class CategoryService {
static async getCategories(): Promise<NewsCategory[]> {
return await prisma.category.findMany({
orderBy: { name: 'asc' }
});
}
static async getCategory(id: string): Promise<NewsCategory | null> {
return await prisma.category.findUnique({
where: { id }
});
}
static async getCategoryBySlug(slug: string): Promise<NewsCategory | null> {
return await prisma.category.findUnique({
where: { slug }
});
}
static async createCategory(data: {
name: string;
slug: string;
description?: string;
color?: string;
}): Promise<NewsCategory> {
return await prisma.category.create({
data
});
}
static async updateCategory(id: string, data: {
name?: string;
slug?: string;
description?: string;
color?: string;
}): Promise<NewsCategory> {
return await prisma.category.update({
where: { id },
data
});
}
static async deleteCategory(id: string): Promise<void> {
await prisma.category.delete({
where: { id }
});
}
}
// Утилиты для миграции данных
export class MigrationService {
static async migrateFromStaticData(staticData: any[]): Promise<void> {
for (const item of staticData) {
try {
await prisma.news.upsert({
where: { slug: item.slug },
update: {
title: item.title,
summary: item.summary,
content: item.content,
category: item.category,
imageUrl: item.imageUrl,
featured: item.featured || false,
published: item.published !== false,
publishedAt: new Date(item.publishedAt),
tags: item.tags || []
},
create: {
title: item.title,
slug: item.slug,
summary: item.summary,
content: item.content,
category: item.category,
imageUrl: item.imageUrl,
featured: item.featured || false,
published: item.published !== false,
publishedAt: new Date(item.publishedAt),
tags: item.tags || []
}
});
} catch (error) {
console.error(`Error migrating news item ${item.slug}:`, error);
}
}
}
static async createDefaultCategories(): Promise<void> {
const categories = [
{ name: 'Новости компании', slug: 'company', color: 'bg-blue-500', description: 'Корпоративные новости и объявления' },
{ name: 'Акции', slug: 'promotions', color: 'bg-green-500', description: 'Специальные предложения и акции' },
{ name: 'Другое', slug: 'other', color: 'bg-purple-500', description: 'Прочие новости и события' }
];
for (const category of categories) {
try {
await prisma.category.upsert({
where: { slug: category.slug },
update: category,
create: category
});
} catch (error) {
console.error(`Error creating category ${category.slug}:`, error);
}
}
}
static async createDefaultAdmin(): Promise<void> {
const { hashPassword } = await import('./auth');
try {
await prisma.user.upsert({
where: { email: 'admin@ckeproekt.ru' },
update: {},
create: {
email: 'admin@ckeproekt.ru',
username: 'admin',
password: await hashPassword('admin123'),
role: 'ADMIN',
name: 'Администратор'
}
});
} catch (error) {
console.error('Error creating default admin:', error);
}
}
}