Add complete CKE Project implementation with news management system
This commit is contained in:
250
lib/auth.ts
Normal file
250
lib/auth.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: 'USER' | 'ADMIN' | 'EDITOR';
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export const JWT_SECRET = process.env.NEXTAUTH_SECRET || 'your-secret-key';
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
|
||||
export function generateToken(userId: string): string {
|
||||
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): { userId: string } | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserFromToken(token: string): Promise<User | null> {
|
||||
const decoded = verifyToken(token);
|
||||
if (!decoded) return null;
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
role: true,
|
||||
name: true,
|
||||
avatar: true
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAuthContext(request: NextRequest): Promise<AuthContext> {
|
||||
const token = extractTokenFromRequest(request);
|
||||
|
||||
if (!token) {
|
||||
return { user: null, isAuthenticated: false };
|
||||
}
|
||||
|
||||
const user = await getUserFromToken(token);
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated: !!user
|
||||
};
|
||||
}
|
||||
|
||||
export function extractTokenFromRequest(request: NextRequest): string | null {
|
||||
// Проверяем заголовок Authorization
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
|
||||
// Проверяем cookie
|
||||
const tokenCookie = request.cookies.get('auth-token');
|
||||
if (tokenCookie) {
|
||||
return tokenCookie.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function requireAuth(context: AuthContext): User {
|
||||
if (!context.isAuthenticated || !context.user) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
return context.user;
|
||||
}
|
||||
|
||||
export function requireRole(context: AuthContext, allowedRoles: string[]): User {
|
||||
const user = requireAuth(context);
|
||||
|
||||
if (!allowedRoles.includes(user.role)) {
|
||||
throw new Error('Insufficient permissions');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export function requireAdmin(context: AuthContext): User {
|
||||
return requireRole(context, ['ADMIN']);
|
||||
}
|
||||
|
||||
export function requireEditorOrAdmin(context: AuthContext): User {
|
||||
return requireRole(context, ['EDITOR', 'ADMIN']);
|
||||
}
|
||||
|
||||
export async function authenticateUser(email: string, password: string): Promise<{ user: User; token: string } | null> {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
role: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
password: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValidPassword = await verifyPassword(password, user.password);
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// Убираем пароль из возвращаемых данных
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
|
||||
return {
|
||||
user: userWithoutPassword,
|
||||
token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerUser(userData: {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
name?: string;
|
||||
role?: 'USER' | 'ADMIN' | 'EDITOR';
|
||||
}): Promise<{ user: User; token: string } | null> {
|
||||
try {
|
||||
// Проверяем, что пользователь с таким email или username не существует
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ email: userData.email },
|
||||
{ username: userData.username }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(userData.password);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: userData.email,
|
||||
username: userData.username,
|
||||
password: hashedPassword,
|
||||
name: userData.name,
|
||||
role: userData.role || 'USER'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
role: true,
|
||||
name: true,
|
||||
avatar: true
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateToken(user.id);
|
||||
|
||||
return {
|
||||
user,
|
||||
token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware для проверки аутентификации в API routes
|
||||
export async function withAuth(
|
||||
handler: (request: NextRequest, context: AuthContext) => Promise<Response>
|
||||
) {
|
||||
return async (request: NextRequest) => {
|
||||
const context = await getAuthContext(request);
|
||||
return handler(request, context);
|
||||
};
|
||||
}
|
||||
|
||||
// Middleware для проверки роли в API routes
|
||||
export async function withRole(
|
||||
handler: (request: NextRequest, context: AuthContext) => Promise<Response>,
|
||||
allowedRoles: string[]
|
||||
) {
|
||||
return withAuth(async (request: NextRequest, context: AuthContext) => {
|
||||
try {
|
||||
requireRole(context, allowedRoles);
|
||||
return handler(request, context);
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Insufficient permissions' }),
|
||||
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Middleware для проверки админских прав
|
||||
export async function withAdmin(
|
||||
handler: (request: NextRequest, context: AuthContext) => Promise<Response>
|
||||
) {
|
||||
return withRole(handler, ['ADMIN']);
|
||||
}
|
605
lib/database.ts
Normal file
605
lib/database.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
455
lib/graphql/resolvers.ts
Normal file
455
lib/graphql/resolvers.ts
Normal file
@ -0,0 +1,455 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
// News queries
|
||||
news: async (_: any, { id }: { id: string }) => {
|
||||
return await prisma.news.findUnique({
|
||||
where: { id },
|
||||
include: { author: true }
|
||||
});
|
||||
},
|
||||
|
||||
newsList: async (_: any, args: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
featured?: boolean;
|
||||
published?: boolean;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
}) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
category,
|
||||
search,
|
||||
featured,
|
||||
published = true,
|
||||
sortBy = 'publishedAt',
|
||||
sortOrder = 'desc'
|
||||
} = args;
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = {};
|
||||
|
||||
if (published !== undefined) {
|
||||
where.published = published;
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (featured !== undefined) {
|
||||
where.featured = featured;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ summary: { contains: search, mode: 'insensitive' } },
|
||||
{ content: { contains: search, mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
const orderBy: any = {};
|
||||
orderBy[sortBy] = sortOrder;
|
||||
|
||||
const [news, total] = await Promise.all([
|
||||
prisma.news.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy,
|
||||
include: { author: true }
|
||||
}),
|
||||
prisma.news.count({ where })
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
news,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1
|
||||
};
|
||||
},
|
||||
|
||||
newsCount: async (_: any, args: {
|
||||
category?: string;
|
||||
search?: string;
|
||||
published?: boolean;
|
||||
}) => {
|
||||
const { category, search, published = true } = args;
|
||||
|
||||
const where: any = {};
|
||||
|
||||
if (published !== undefined) {
|
||||
where.published = published;
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ summary: { contains: search, mode: 'insensitive' } },
|
||||
{ content: { contains: search, mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
return await prisma.news.count({ where });
|
||||
},
|
||||
|
||||
// Category queries
|
||||
categories: async () => {
|
||||
return await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
},
|
||||
|
||||
category: async (_: any, { id }: { id: string }) => {
|
||||
return await prisma.category.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
},
|
||||
|
||||
// User queries
|
||||
user: async (_: any, { id }: { id: string }) => {
|
||||
return await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { news: true }
|
||||
});
|
||||
},
|
||||
|
||||
users: async () => {
|
||||
return await prisma.user.findMany({
|
||||
include: { news: true }
|
||||
});
|
||||
},
|
||||
|
||||
me: async (_: any, __: any, context: any) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
return await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { news: true }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
// News mutations
|
||||
createNews: async (_: any, { input }: { input: any }, context: any) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const news = await prisma.news.create({
|
||||
data: {
|
||||
...input,
|
||||
authorId: context.user.id,
|
||||
publishedAt: input.publishedAt ? new Date(input.publishedAt) : new Date()
|
||||
},
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
updateNews: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const existingNews = await prisma.news.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existingNews) {
|
||||
throw new Error('News not found');
|
||||
}
|
||||
|
||||
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const updateData: any = { ...input };
|
||||
if (input.publishedAt) {
|
||||
updateData.publishedAt = new Date(input.publishedAt);
|
||||
}
|
||||
|
||||
const news = await prisma.news.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
deleteNews: async (_: any, { id }: { id: string }, context: any) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const existingNews = await prisma.news.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existingNews) {
|
||||
throw new Error('News not found');
|
||||
}
|
||||
|
||||
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
await prisma.news.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
toggleNewsPublished: async (_: any, { id }: { id: string }, context: any) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const existingNews = await prisma.news.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existingNews) {
|
||||
throw new Error('News not found');
|
||||
}
|
||||
|
||||
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const news = await prisma.news.update({
|
||||
where: { id },
|
||||
data: { published: !existingNews.published },
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
toggleNewsFeatured: async (_: any, { id }: { id: string }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const existingNews = await prisma.news.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existingNews) {
|
||||
throw new Error('News not found');
|
||||
}
|
||||
|
||||
const news = await prisma.news.update({
|
||||
where: { id },
|
||||
data: { featured: !existingNews.featured },
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
incrementNewsViews: async (_: any, { id }: { id: string }) => {
|
||||
const news = await prisma.news.update({
|
||||
where: { id },
|
||||
data: { views: { increment: 1 } },
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
likeNews: async (_: any, { id }: { id: string }) => {
|
||||
const news = await prisma.news.update({
|
||||
where: { id },
|
||||
data: { likes: { increment: 1 } },
|
||||
include: { author: true }
|
||||
});
|
||||
|
||||
return news;
|
||||
},
|
||||
|
||||
// Category mutations
|
||||
createCategory: async (_: any, { input }: { input: any }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: input
|
||||
});
|
||||
|
||||
return category;
|
||||
},
|
||||
|
||||
updateCategory: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const category = await prisma.category.update({
|
||||
where: { id },
|
||||
data: input
|
||||
});
|
||||
|
||||
return category;
|
||||
},
|
||||
|
||||
deleteCategory: async (_: any, { id }: { id: string }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
await prisma.category.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// User mutations
|
||||
createUser: async (_: any, { input }: { input: any }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(input.password, 12);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
...input,
|
||||
password: hashedPassword
|
||||
},
|
||||
include: { news: true }
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
updateUser: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
|
||||
if (!context.user || (context.user.id !== id && context.user.role !== 'ADMIN')) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const updateData: any = { ...input };
|
||||
if (input.password) {
|
||||
updateData.password = await bcrypt.hash(input.password, 12);
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: { news: true }
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
deleteUser: async (_: any, { id }: { id: string }, context: any) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
await prisma.user.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Auth mutations
|
||||
login: async (_: any, { email, password }: { email: string; password: string }) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.NEXTAUTH_SECRET || 'secret',
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
user
|
||||
};
|
||||
},
|
||||
|
||||
register: async (_: any, { input }: { input: any }) => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ email: input.email },
|
||||
{ username: input.username }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(input.password, 12);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
...input,
|
||||
password: hashedPassword
|
||||
}
|
||||
});
|
||||
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.NEXTAUTH_SECRET || 'secret',
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
user
|
||||
};
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
// В GraphQL logout обычно обрабатывается на клиенте
|
||||
// путем удаления токена из localStorage/cookies
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
182
lib/graphql/schema.ts
Normal file
182
lib/graphql/schema.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
export const typeDefs = gql`
|
||||
type News {
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
summary: String!
|
||||
content: String!
|
||||
category: String!
|
||||
imageUrl: String
|
||||
featured: Boolean!
|
||||
published: Boolean!
|
||||
publishedAt: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
authorId: String
|
||||
author: User
|
||||
views: Int!
|
||||
likes: Int!
|
||||
tags: [String!]!
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
username: String!
|
||||
role: UserRole!
|
||||
name: String
|
||||
avatar: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
news: [News!]!
|
||||
}
|
||||
|
||||
type Category {
|
||||
id: ID!
|
||||
name: String!
|
||||
slug: String!
|
||||
description: String
|
||||
color: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
USER
|
||||
ADMIN
|
||||
EDITOR
|
||||
}
|
||||
|
||||
type Query {
|
||||
# News queries
|
||||
news(id: ID!): News
|
||||
newsList(
|
||||
page: Int = 1
|
||||
limit: Int = 10
|
||||
category: String
|
||||
search: String
|
||||
featured: Boolean
|
||||
published: Boolean = true
|
||||
sortBy: String = "publishedAt"
|
||||
sortOrder: String = "desc"
|
||||
): NewsListResponse!
|
||||
newsCount(category: String, search: String, published: Boolean = true): Int!
|
||||
|
||||
# Category queries
|
||||
categories: [Category!]!
|
||||
category(id: ID!): Category
|
||||
|
||||
# User queries
|
||||
user(id: ID!): User
|
||||
users: [User!]!
|
||||
me: User
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# News mutations
|
||||
createNews(input: CreateNewsInput!): News!
|
||||
updateNews(id: ID!, input: UpdateNewsInput!): News!
|
||||
deleteNews(id: ID!): Boolean!
|
||||
toggleNewsPublished(id: ID!): News!
|
||||
toggleNewsFeatured(id: ID!): News!
|
||||
incrementNewsViews(id: ID!): News!
|
||||
likeNews(id: ID!): News!
|
||||
|
||||
# Category mutations
|
||||
createCategory(input: CreateCategoryInput!): Category!
|
||||
updateCategory(id: ID!, input: UpdateCategoryInput!): Category!
|
||||
deleteCategory(id: ID!): Boolean!
|
||||
|
||||
# User mutations
|
||||
createUser(input: CreateUserInput!): User!
|
||||
updateUser(id: ID!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: ID!): Boolean!
|
||||
|
||||
# Auth mutations
|
||||
login(email: String!, password: String!): AuthResponse!
|
||||
register(input: RegisterInput!): AuthResponse!
|
||||
logout: Boolean!
|
||||
}
|
||||
|
||||
type NewsListResponse {
|
||||
news: [News!]!
|
||||
total: Int!
|
||||
page: Int!
|
||||
limit: Int!
|
||||
totalPages: Int!
|
||||
hasNextPage: Boolean!
|
||||
hasPrevPage: Boolean!
|
||||
}
|
||||
|
||||
type AuthResponse {
|
||||
token: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
input CreateNewsInput {
|
||||
title: String!
|
||||
slug: String!
|
||||
summary: String!
|
||||
content: String!
|
||||
category: String!
|
||||
imageUrl: String
|
||||
featured: Boolean = false
|
||||
published: Boolean = true
|
||||
publishedAt: String
|
||||
tags: [String!] = []
|
||||
}
|
||||
|
||||
input UpdateNewsInput {
|
||||
title: String
|
||||
slug: String
|
||||
summary: String
|
||||
content: String
|
||||
category: String
|
||||
imageUrl: String
|
||||
featured: Boolean
|
||||
published: Boolean
|
||||
publishedAt: String
|
||||
tags: [String!]
|
||||
}
|
||||
|
||||
input CreateCategoryInput {
|
||||
name: String!
|
||||
slug: String!
|
||||
description: String
|
||||
color: String = "#3B82F6"
|
||||
}
|
||||
|
||||
input UpdateCategoryInput {
|
||||
name: String
|
||||
slug: String
|
||||
description: String
|
||||
color: String
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
username: String!
|
||||
password: String!
|
||||
role: UserRole = USER
|
||||
name: String
|
||||
avatar: String
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
email: String
|
||||
username: String
|
||||
password: String
|
||||
role: UserRole
|
||||
name: String
|
||||
avatar: String
|
||||
}
|
||||
|
||||
input RegisterInput {
|
||||
email: String!
|
||||
username: String!
|
||||
password: String!
|
||||
name: String
|
||||
}
|
||||
`;
|
184
lib/news-data.ts
Normal file
184
lib/news-data.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { NewsItem } from './types';
|
||||
|
||||
export const NEWS_DATA: NewsItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Новые возможности экспертизы недвижимости',
|
||||
summary: 'Мы расширили спектр услуг по экспертизе недвижимости, добавив новые методы оценки и анализа.',
|
||||
content: `
|
||||
<p>Компания CKE Project рада объявить о значительном расширении наших услуг по экспертизе недвижимости. Мы внедрили новейшие технологии и методики оценки, которые позволяют нам предоставлять еще более точные и детальные отчеты.</p>
|
||||
|
||||
<h3>Что нового:</h3>
|
||||
<ul>
|
||||
<li>Тепловизионная диагностика зданий</li>
|
||||
<li>3D-сканирование помещений</li>
|
||||
<li>Анализ влажности и микроклимата</li>
|
||||
<li>Оценка энергоэффективности</li>
|
||||
</ul>
|
||||
|
||||
<p>Эти нововведения позволяют нам выявлять скрытые дефекты и проблемы, которые могут повлиять на стоимость и безопасность недвижимости.</p>
|
||||
`,
|
||||
category: 'company',
|
||||
publishedAt: '2024-01-15T10:00:00Z',
|
||||
imageUrl: '/images/office.jpg',
|
||||
slug: 'novye-vozmozhnosti-ekspertizy-nedvizhimosti',
|
||||
featured: true,
|
||||
published: true,
|
||||
createdAt: '2024-01-15T09:00:00Z',
|
||||
updatedAt: '2024-01-15T09:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Скидка 20% на строительный контроль',
|
||||
summary: 'Специальное предложение для новых клиентов - скидка 20% на услуги строительного контроля.',
|
||||
content: `
|
||||
<p>До конца месяца действует специальное предложение для новых клиентов - скидка 20% на все услуги строительного контроля!</p>
|
||||
|
||||
<h3>Условия акции:</h3>
|
||||
<ul>
|
||||
<li>Скидка действует только для новых клиентов</li>
|
||||
<li>Минимальная сумма заказа - 50 000 рублей</li>
|
||||
<li>Акция действует до 31 января 2024 года</li>
|
||||
<li>Скидка не суммируется с другими предложениями</li>
|
||||
</ul>
|
||||
|
||||
<p>Не упустите возможность получить качественные услуги строительного контроля по выгодной цене!</p>
|
||||
`,
|
||||
category: 'promotions',
|
||||
publishedAt: '2024-01-10T14:30:00Z',
|
||||
imageUrl: '/images/placeholders/services/construction-control.jpg',
|
||||
slug: 'skidka-20-na-stroitelnyj-kontrol',
|
||||
featured: true,
|
||||
published: true,
|
||||
createdAt: '2024-01-10T14:00:00Z',
|
||||
updatedAt: '2024-01-10T14:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Получена лицензия СРО',
|
||||
summary: 'Наша компания успешно получила лицензию СРО, что подтверждает высокое качество наших услуг.',
|
||||
content: `
|
||||
<p>CKE Project гордится сообщить о получении лицензии СРО (Саморегулируемая организация), что является важным этапом в развитии нашей компании.</p>
|
||||
|
||||
<p>Получение лицензии СРО подтверждает:</p>
|
||||
<ul>
|
||||
<li>Соответствие всем требованиям и стандартам отрасли</li>
|
||||
<li>Высокую квалификацию наших специалистов</li>
|
||||
<li>Надежность и профессионализм компании</li>
|
||||
<li>Возможность выполнения работ любой сложности</li>
|
||||
</ul>
|
||||
|
||||
<p>Теперь мы можем предоставлять еще более широкий спектр услуг с официальными гарантиями качества.</p>
|
||||
`,
|
||||
category: 'company',
|
||||
publishedAt: '2024-01-05T09:00:00Z',
|
||||
imageUrl: '/images/certificates/sro.jpg',
|
||||
slug: 'poluchena-licenziya-sro',
|
||||
featured: false,
|
||||
published: true,
|
||||
createdAt: '2024-01-05T08:00:00Z',
|
||||
updatedAt: '2024-01-05T08:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Новый офис в центре города',
|
||||
summary: 'Открытие нового офиса в центре города для удобства наших клиентов.',
|
||||
content: `
|
||||
<p>Мы рады объявить об открытии нового офиса в центре города по адресу: ул. Центральная, 123.</p>
|
||||
|
||||
<h3>Преимущества нового офиса:</h3>
|
||||
<ul>
|
||||
<li>Удобное расположение в центре города</li>
|
||||
<li>Современное оборудование и технологии</li>
|
||||
<li>Комфортные условия для консультаций</li>
|
||||
<li>Парковка для клиентов</li>
|
||||
</ul>
|
||||
|
||||
<p>Часы работы: пн-пт с 9:00 до 18:00, сб с 10:00 до 16:00.</p>
|
||||
`,
|
||||
category: 'company',
|
||||
publishedAt: '2024-01-01T12:00:00Z',
|
||||
imageUrl: '/images/office.jpg',
|
||||
slug: 'novyj-ofis-v-centre-goroda',
|
||||
featured: false,
|
||||
published: false,
|
||||
createdAt: '2024-01-01T11:00:00Z',
|
||||
updatedAt: '2024-01-01T11:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Праздничные скидки на все услуги',
|
||||
summary: 'Новогодние скидки до 30% на все виды экспертизы и консультаций.',
|
||||
content: `
|
||||
<p>В честь новогодних праздников мы предлагаем скидки до 30% на все наши услуги!</p>
|
||||
|
||||
<h3>Размер скидок:</h3>
|
||||
<ul>
|
||||
<li>Экспертиза недвижимости - 25%</li>
|
||||
<li>Строительный контроль - 20%</li>
|
||||
<li>Техническое обследование - 30%</li>
|
||||
<li>Консультации - 15%</li>
|
||||
</ul>
|
||||
|
||||
<p>Акция действует с 25 декабря 2023 по 15 января 2024 года.</p>
|
||||
`,
|
||||
category: 'promotions',
|
||||
publishedAt: '2023-12-25T10:00:00Z',
|
||||
imageUrl: '/images/placeholders/services/house-recognition.jpg',
|
||||
slug: 'prazdnichnye-skidki-na-vse-uslugi',
|
||||
featured: false,
|
||||
published: true,
|
||||
createdAt: '2023-12-25T09:00:00Z',
|
||||
updatedAt: '2023-12-25T09:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Обновление программного обеспечения',
|
||||
summary: 'Внедрение нового ПО для повышения качества и скорости выполнения работ.',
|
||||
content: `
|
||||
<p>Мы постоянно совершенствуем наши технологии и рады сообщить о внедрении нового программного обеспечения для анализа и обработки данных.</p>
|
||||
|
||||
<h3>Новые возможности:</h3>
|
||||
<ul>
|
||||
<li>Автоматизированная обработка измерений</li>
|
||||
<li>Создание 3D-моделей объектов</li>
|
||||
<li>Быстрое формирование отчетов</li>
|
||||
<li>Интеграция с государственными базами данных</li>
|
||||
</ul>
|
||||
|
||||
<p>Это позволит нам выполнять работы быстрее и с еще большей точностью.</p>
|
||||
`,
|
||||
category: 'other',
|
||||
publishedAt: '2023-12-20T16:00:00Z',
|
||||
slug: 'obnovlenie-programmnogo-obespecheniya',
|
||||
featured: false,
|
||||
published: true,
|
||||
createdAt: '2023-12-20T15:00:00Z',
|
||||
updatedAt: '2023-12-20T15:00:00Z'
|
||||
}
|
||||
];
|
||||
|
||||
export function getNewsById(id: string): NewsItem | undefined {
|
||||
return NEWS_DATA.find(news => news.id === id);
|
||||
}
|
||||
|
||||
export function getNewsBySlug(slug: string): NewsItem | undefined {
|
||||
return NEWS_DATA.find(news => news.slug === slug);
|
||||
}
|
||||
|
||||
export function getNewsByCategory(category: string): NewsItem[] {
|
||||
const publishedNews = NEWS_DATA.filter(news => news.published !== false);
|
||||
if (category === 'all') return publishedNews;
|
||||
return publishedNews.filter(news => news.category === category);
|
||||
}
|
||||
|
||||
export function getFeaturedNews(): NewsItem[] {
|
||||
return NEWS_DATA.filter(news => news.featured && news.published !== false);
|
||||
}
|
||||
|
||||
export function getLatestNews(limit: number = 3): NewsItem[] {
|
||||
return NEWS_DATA
|
||||
.filter(news => news.published !== false)
|
||||
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
|
||||
.slice(0, limit);
|
||||
}
|
63
lib/types.ts
Normal file
63
lib/types.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export interface NewsItem {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
category: NewsCategory;
|
||||
publishedAt: string;
|
||||
imageUrl?: string;
|
||||
slug: string;
|
||||
featured?: boolean;
|
||||
published?: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export type NewsCategory = 'company' | 'promotions' | 'other';
|
||||
|
||||
export interface NewsCategoryInfo {
|
||||
id: NewsCategory;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const NEWS_CATEGORIES: NewsCategoryInfo[] = [
|
||||
{
|
||||
id: 'company',
|
||||
name: 'Новости компании',
|
||||
description: 'Корпоративные новости и обновления',
|
||||
color: 'bg-blue-500'
|
||||
},
|
||||
{
|
||||
id: 'promotions',
|
||||
name: 'Акции',
|
||||
description: 'Специальные предложения и скидки',
|
||||
color: 'bg-green-500'
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
name: 'Другое',
|
||||
description: 'Прочие новости и объявления',
|
||||
color: 'bg-gray-500'
|
||||
}
|
||||
];
|
||||
|
||||
// Типы для административной панели
|
||||
export interface AdminUser {
|
||||
id: string;
|
||||
username: string;
|
||||
role: 'admin' | 'editor';
|
||||
}
|
||||
|
||||
export interface NewsFormData {
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
category: NewsCategory;
|
||||
imageUrl?: string;
|
||||
featured: boolean;
|
||||
published: boolean;
|
||||
publishedAt: string;
|
||||
tags: string[];
|
||||
}
|
Reference in New Issue
Block a user