Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions actions/get-total-revenue.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
import { cache } from 'react';
import { z } from 'zod';

import prismadb from '@/lib/prismadb';

export const getTotalRevenue = async (storeId: string) => {
const paidOrders = await prismadb.order.findMany({
where: {
storeId,
isPaid: true,
},
include: {
orderItems: {
include: {
product: true,
const storeIdSchema = z.string().uuid('Неверный формат идентификатора магазина');

interface PrismaOrder {
orderItems: {
product: {
price: number;
};
}[];
}

// Кэшированная функция получения выручки
export const getTotalRevenue = cache(async (storeId: string) => {
try {
const validationResult = storeIdSchema.safeParse(storeId);

if (!validationResult.success) {
throw new Error('Неверный идентификатор магазина');
}

const paidOrders = await prismadb.order.findMany({
where: {
storeId: validationResult.data,
isPaid: true,
},
select: {
orderItems: {
select: {
product: {
select: {
price: true,
},
},
},
},
},
},
});
});

return paidOrders.reduce((total: any, order: any) => {
const orderTotal = order.orderItems.reduce(
(orderSum: any, item: any) => orderSum + item.product.price.toNumber(),
0,
);
return total + orderTotal;
}, 0);
};
return paidOrders.reduce((total: number, order: PrismaOrder) => {
const orderTotal = order.orderItems.reduce(
(orderSum: number, item) =>
orderSum + item.product.price,
0,
);
return total + orderTotal;
}, 0);
} catch (error) {
console.error('[GET_TOTAL_REVENUE]', error);
return 0;
}
});
28 changes: 22 additions & 6 deletions app/(root)/(routes)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
'use client';

import { useEffect } from 'react';
import { useEffect, useCallback } from 'react';

import { useStoreModal } from '@/hooks/use-store-modal';

// Компонент настройки магазина
const SetupPage = () => {
const onOpen = useStoreModal((state) => state.onOpen);
const isOpen = useStoreModal((state) => state.isOpen);

// Используем селекторы из хука с мемоизацией
const { onOpen, isOpen } = useStoreModal(
useCallback(
(state) => ({
onOpen: state.onOpen,
isOpen: state.isOpen,
}),
[],
),
);

useEffect(() => {
if (!isOpen) {
onOpen();
try {
if (!isOpen) {
onOpen();
}
} catch (error) {
console.error('[SETUP_PAGE]', error);
}
}, [isOpen, onOpen]);

return null;
// Возвращаем пустой фрагмент вместо null для лучшей производительности
return <></>;
};

export default SetupPage;
48 changes: 35 additions & 13 deletions app/(root)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
import { cache } from 'react';
import { z } from 'zod';

import prismadb from '@/lib/prismadb';

// Кэширование запроса на получение магазина
const getStore = cache(async (userId: string) => {
return prismadb.store.findFirst({
where: { userId },
select: {
id: true,
name: true,
createdAt: true,
},
});
});

// Схема валидации для userId
const userIdSchema = z.string().min(1, 'ID пользователя обязателен');

export default async function SetupLayout({ children }: { children: React.ReactNode }) {
const { userId }: { userId: string | null } = await auth();
try {
const { userId } = await auth();

if (!userId) {
redirect('/sign-in');
}
// Валидация userId
const validationResult = userIdSchema.safeParse(userId);

const store = await prismadb.store.findFirst({
where: {
userId,
},
});
if (!validationResult.success) {
redirect('/sign-in');
}

if (store) {
redirect(`/${store.id}`);
}
// Используем кэшированную функцию для получения магазина
const store = await getStore(validationResult.data);

return <>{children}</>;
if (store) {
redirect(`/${store.id}`);
}

return <div className="min-h-screen">{children}</div>;
} catch (error) {
console.error('[SETUP_LAYOUT]', error);
redirect('/error');
}
}
147 changes: 134 additions & 13 deletions app/api/[storeId]/billboards/[billboardId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,148 @@
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { cache } from 'react';

import prismadb from '@/lib/prismadb';

export async function GET(request: Request, props: { params: Promise<{ billboardId: string }> }) {
const params = await props.params;
import { errorResponses } from '@/lib/error-responses';

// Кэширование запросов на получение билбордов
const getBillboards = cache(async (storeId: string) => {
return prismadb.billboard.findMany({
where: { storeId },
select: {
id: true,
label: true,
imageUrl: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
});
});

// Расширенная схема валидации для создания билборда
const billboardSchema = z.object({
label: z.string().min(1, 'Укажите метку').max(100, 'Метка слишком длинная').trim(),
imageUrl: z
.string()
.url('Укажите корректный URL изображения')
.max(500, 'URL слишком длинный')
.refine((url) => url.startsWith('https://'), 'Разрешены только HTTPS URL'),
});

export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) {
try {
if (!params.billboardId) {
return new NextResponse('Необходим идентификатор билборда.', {
status: 400,
});
const params = await props.params;
const { userId } = await auth();

if (!userId) {
return errorResponses.unauthorized();
}

const billboard = await prismadb.billboard.findUnique({
where: {
id: params.billboardId,
if (!params.storeId) {
return errorResponses.badRequest('Необходим идентификатор магазина');
}

const body = await request.json();
const validationResult = billboardSchema.safeParse(body);

if (!validationResult.success) {
return errorResponses.validationError(validationResult.error.message);
}

// Проверка на лимит билбордов для магазина
const billboardCount = await prismadb.billboard.count({
where: { storeId: params.storeId },
});

if (billboardCount >= 50) {
return errorResponses.badRequest('Достигнут лимит билбордов для магазина');
}

// Используем транзакцию для проверки прав и создания билборда
const billboard = await prismadb.$transaction(async (tx) => {
const storeByUserId = await tx.store.findFirst({
where: {
id: params.storeId,
userId,
},
select: { id: true },
});

if (!storeByUserId) {
throw new Error('Не авторизованный доступ');
}

// Проверка на дубликаты
const existingBillboard = await tx.billboard.findFirst({
where: {
storeId: params.storeId,
label: validationResult.data.label,
},
select: { id: true },
});

if (existingBillboard) {
throw new Error('Билборд с такой меткой уже существует');
}

return tx.billboard.create({
data: {
...validationResult.data,
storeId: params.storeId,
},
select: {
id: true,
label: true,
imageUrl: true,
createdAt: true,
},
});
});

return NextResponse.json(billboard, {
status: 201,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});
} catch (error) {
console.error('[BILLBOARDS_POST]', error);
if (error instanceof Error) {
switch (error.message) {
case 'Не авторизованный доступ':
return errorResponses.forbidden();
case 'Билборд с такой меткой уже существует':
return errorResponses.conflict(error.message);
default:
return errorResponses.serverError(error);
}
}
return errorResponses.serverError(error);
}
}

return NextResponse.json(billboard);
export async function GET(request: Request, props: { params: Promise<{ storeId: string }> }) {
try {
const params = await props.params;

if (!params.storeId) {
return errorResponses.badRequest('Необходим идентификатор магазина');
}

// Используем кэшированную функцию для получения билбордов
const billboards = await getBillboards(params.storeId);

return NextResponse.json(billboards, {
headers: {
'Cache-Control': 'public, max-age=60, stale-while-revalidate=30',
},
});
} catch (error) {
console.log('[BILLBOARD_GET]', error);
return new NextResponse('Ошибка сервера', { status: 500 });
console.error('[BILLBOARDS_GET]', error);
return errorResponses.serverError(error);
}
}

Expand Down
Loading