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

250
lib/auth.ts Normal file
View 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
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);
}
}
}

455
lib/graphql/resolvers.ts Normal file
View 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
View 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
View 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
View 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[];
}