diff --git a/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc b/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc index 291f164..2fa74e5 100644 Binary files a/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc and b/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc differ diff --git a/backend/app/routers/catalog_router.py b/backend/app/routers/catalog_router.py index 0308933..8bab70e 100644 --- a/backend/app/routers/catalog_router.py +++ b/backend/app/routers/catalog_router.py @@ -443,22 +443,50 @@ async def get_products_endpoint( skip: int = 0, limit: int = 100, category_id: Optional[int] = None, + category_ids: Optional[str] = None, # Добавляем параметр category_ids в виде строки с разделителями-запятыми collection_id: Optional[int] = None, + collection_ids: Optional[str] = None, # Добавляем параметр collection_ids в виде строки с разделителями-запятыми search: Optional[str] = None, min_price: Optional[float] = None, max_price: Optional[float] = None, is_active: Optional[bool] = True, sort_by: Optional[str] = None, sort_order: Optional[str] = "asc", + size_ids: Optional[str] = None, # Параметр size_ids в виде строки с разделителями-запятыми + sort: Optional[str] = None, # Параметр sort для прямой передачи опции сортировки db: Session = Depends(get_db) ): # Формируем фильтры для Meilisearch filters = [] - if category_id is not None: + # Обработка фильтрации по категориям + if category_ids: + try: + # Преобразуем строку с ID категорий в список + category_ids_list = [int(cat_id.strip()) for cat_id in category_ids.split(',') if cat_id.strip()] + if category_ids_list: + # Формируем условие для фильтрации по категориям + category_filter = " OR ".join([f"category_id = {cat_id}" for cat_id in category_ids_list]) + filters.append(f"({category_filter})") + except ValueError as e: + logging.warning(f"Некорректный формат category_ids: {category_ids}. Ошибка: {str(e)}") + # Для обратной совместимости + elif category_id is not None: filters.append(f"category_id = {category_id}") - if collection_id is not None: + # Обработка фильтрации по коллекциям + if collection_ids: + try: + # Преобразуем строку с ID коллекций в список + collection_ids_list = [int(coll_id.strip()) for coll_id in collection_ids.split(',') if coll_id.strip()] + if collection_ids_list: + # Формируем условие для фильтрации по коллекциям + collection_filter = " OR ".join([f"collection_id = {coll_id}" for coll_id in collection_ids_list]) + filters.append(f"({collection_filter})") + except ValueError as e: + logging.warning(f"Некорректный формат collection_ids: {collection_ids}. Ошибка: {str(e)}") + # Для обратной совместимости + elif collection_id is not None: filters.append(f"collection_id = {collection_id}") if is_active is not None: @@ -470,10 +498,39 @@ async def get_products_endpoint( if max_price is not None: filters.append(f"price <= {max_price}") + # Обработка фильтрации по размерам + if size_ids: + try: + # Преобразуем строку с ID размеров в список + size_ids_list = [int(size_id.strip()) for size_id in size_ids.split(',') if size_id.strip()] + if size_ids_list: + # Формируем условие для фильтрации по размерам + size_filter = " OR ".join([f"size_ids = {size_id}" for size_id in size_ids_list]) + filters.append(f"({size_filter})") + except ValueError as e: + logging.warning(f"Некорректный формат size_ids: {size_ids}. Ошибка: {str(e)}") + # Если формат некорректный, игнорируем этот фильтр + # Формируем параметры сортировки - sort = None - if sort_by: - sort = [f"{sort_by}:{sort_order}"] + sort_param = None + + # Если передан прямой параметр sort, используем его + if sort: + # Обрабатываем популярные варианты сортировки + if sort == 'popular': + sort_param = None # Используем сортировку по умолчанию + elif sort == 'price_asc': + sort_param = ['price:asc'] + elif sort == 'price_desc': + sort_param = ['price:desc'] + elif sort == 'newest': + sort_param = ['created_at:desc'] + else: + # Если передан другой вариант, используем его напрямую + sort_param = [sort] + # Если передан sort_by, используем его + elif sort_by: + sort_param = [f"{sort_by}:{sort_order}"] # Используем Meilisearch для поиска продуктов from app.services import meilisearch_service @@ -485,7 +542,7 @@ async def get_products_endpoint( result = meilisearch_service.search_products( query=search or "", filters=filter_str, - sort=sort, + sort=sort_param, limit=limit, offset=skip ) diff --git a/backend/app/scripts/__pycache__/sync_meilisearch.cpython-310.pyc b/backend/app/scripts/__pycache__/sync_meilisearch.cpython-310.pyc index abb170f..44ccc18 100644 Binary files a/backend/app/scripts/__pycache__/sync_meilisearch.cpython-310.pyc and b/backend/app/scripts/__pycache__/sync_meilisearch.cpython-310.pyc differ diff --git a/backend/app/scripts/sync_meilisearch.py b/backend/app/scripts/sync_meilisearch.py index baefc21..774a581 100644 --- a/backend/app/scripts/sync_meilisearch.py +++ b/backend/app/scripts/sync_meilisearch.py @@ -37,9 +37,10 @@ def format_product_for_meilisearch(product, variants, images): "created_at": product.created_at.isoformat() if product.created_at else None, "updated_at": product.updated_at.isoformat() if product.updated_at else None, "variants": [], - "images": [] + "images": [], + "size_ids": [] # Добавляем массив для хранения ID размеров } - + # Добавляем варианты продукта for variant in variants: variant_dict = { @@ -49,7 +50,7 @@ def format_product_for_meilisearch(product, variants, images): "stock": variant.stock, "is_active": variant.is_active } - + # Добавляем информацию о размере, если она доступна if variant.size: variant_dict["size"] = { @@ -57,9 +58,13 @@ def format_product_for_meilisearch(product, variants, images): "name": variant.size.name, "code": variant.size.code } - + + # Добавляем ID размера в массив size_ids для фильтрации + if variant.size_id not in product_dict["size_ids"]: + product_dict["size_ids"].append(variant.size_id) + product_dict["variants"].append(variant_dict) - + # Добавляем изображения продукта for image in images: image_dict = { @@ -69,11 +74,11 @@ def format_product_for_meilisearch(product, variants, images): "is_primary": image.is_primary } product_dict["images"].append(image_dict) - + # Добавляем основное изображение в корень продукта для удобства if image.is_primary: product_dict["primary_image"] = image.image_url - + return product_dict @@ -127,10 +132,10 @@ def sync_products(db: Session): Синхронизирует все продукты с Meilisearch. """ logger.info("Syncing products...") - + # Получаем все продукты из базы данных products = db.query(Product).all() - + # Форматируем продукты для Meilisearch products_data = [] for product in products: @@ -138,15 +143,15 @@ def sync_products(db: Session): images = db.query(ProductImage).filter(ProductImage.product_id == product.id).all() product_data = format_product_for_meilisearch(product, variants, images) products_data.append(product_data) - + # Синхронизируем продукты с Meilisearch success = meilisearch_service.sync_all_products(products_data) - + if success: logger.info(f"Successfully synced {len(products_data)} products") else: logger.error("Failed to sync products") - + return success @@ -155,21 +160,21 @@ def sync_categories(db: Session): Синхронизирует все категории с Meilisearch. """ logger.info("Syncing categories...") - + # Получаем все категории из базы данных categories = db.query(Category).all() - + # Форматируем категории для Meilisearch categories_data = [format_category_for_meilisearch(category) for category in categories] - + # Синхронизируем категории с Meilisearch success = meilisearch_service.sync_all_categories(categories_data) - + if success: logger.info(f"Successfully synced {len(categories_data)} categories") else: logger.error("Failed to sync categories") - + return success @@ -178,21 +183,21 @@ def sync_collections(db: Session): Синхронизирует все коллекции с Meilisearch. """ logger.info("Syncing collections...") - + # Получаем все коллекции из базы данных collections = db.query(Collection).all() - + # Форматируем коллекции для Meilisearch collections_data = [format_collection_for_meilisearch(collection) for collection in collections] - + # Синхронизируем коллекции с Meilisearch success = meilisearch_service.sync_all_collections(collections_data) - + if success: logger.info(f"Successfully synced {len(collections_data)} collections") else: logger.error("Failed to sync collections") - + return success @@ -201,21 +206,21 @@ def sync_sizes(db: Session): Синхронизирует все размеры с Meilisearch. """ logger.info("Syncing sizes...") - + # Получаем все размеры из базы данных sizes = db.query(Size).all() - + # Форматируем размеры для Meilisearch sizes_data = [format_size_for_meilisearch(size) for size in sizes] - + # Синхронизируем размеры с Meilisearch success = meilisearch_service.sync_all_sizes(sizes_data) - + if success: logger.info(f"Successfully synced {len(sizes_data)} sizes") else: logger.error("Failed to sync sizes") - + return success @@ -224,20 +229,20 @@ def main(): Основная функция для синхронизации всех данных с Meilisearch. """ logger.info("Starting Meilisearch synchronization...") - + # Инициализируем индексы в Meilisearch meilisearch_service.initialize_indexes() - + # Создаем сессию базы данных db = SessionLocal() - + try: # Синхронизируем все данные sync_categories(db) sync_collections(db) sync_sizes(db) sync_products(db) - + logger.info("Meilisearch synchronization completed successfully") except Exception as e: logger.error(f"Error during Meilisearch synchronization: {str(e)}") diff --git a/backend/app/services/__pycache__/meilisearch_service.cpython-310.pyc b/backend/app/services/__pycache__/meilisearch_service.cpython-310.pyc index 6d5715c..665c413 100644 Binary files a/backend/app/services/__pycache__/meilisearch_service.cpython-310.pyc and b/backend/app/services/__pycache__/meilisearch_service.cpython-310.pyc differ diff --git a/backend/app/services/meilisearch_service.py b/backend/app/services/meilisearch_service.py index 84729b2..8534788 100644 --- a/backend/app/services/meilisearch_service.py +++ b/backend/app/services/meilisearch_service.py @@ -28,7 +28,7 @@ def initialize_indexes(): # Настраиваем фильтруемые атрибуты для продуктов client.index(PRODUCT_INDEX).update_filterable_attributes([ - "category_id", "collection_id", "is_active", "price", "discount_price", "slug", "id" + "category_id", "collection_id", "is_active", "price", "discount_price", "slug", "id", "size_ids" ]) # Настраиваем сортируемые атрибуты для продуктов diff --git a/backend/update_and_sync_meilisearch.py b/backend/update_and_sync_meilisearch.py new file mode 100644 index 0000000..d72422c --- /dev/null +++ b/backend/update_and_sync_meilisearch.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +import sys +import logging +from pathlib import Path + +# Добавляем родительскую директорию в sys.path, чтобы импортировать модули приложения +sys.path.append(str(Path(__file__).parent)) + +from app.core import SessionLocal +from app.scripts.sync_meilisearch import sync_products, sync_categories, sync_collections, sync_sizes +from app.services.meilisearch_service import initialize_indexes + +# Настройка логгера +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def main(): + """ + Обновляет настройки Meilisearch и синхронизирует данные. + """ + logger.info("Обновление настроек Meilisearch и синхронизация данных...") + + try: + # Инициализируем индексы в Meilisearch с обновленными настройками + logger.info("Инициализация индексов Meilisearch...") + initialize_indexes() + + # Создаем сессию базы данных + db = SessionLocal() + + try: + # Синхронизируем все данные + logger.info("Синхронизация категорий...") + sync_categories(db) + + logger.info("Синхронизация коллекций...") + sync_collections(db) + + logger.info("Синхронизация размеров...") + sync_sizes(db) + + logger.info("Синхронизация продуктов...") + sync_products(db) + + logger.info("Синхронизация данных завершена успешно!") + finally: + db.close() + except Exception as e: + logger.error(f"Ошибка при обновлении настроек и синхронизации данных: {str(e)}") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/update_meilisearch_settings.py b/backend/update_meilisearch_settings.py index 157b4c8..49e54e2 100644 --- a/backend/update_meilisearch_settings.py +++ b/backend/update_meilisearch_settings.py @@ -13,56 +13,56 @@ def main(): Скрипт для обновления настроек индексов Meilisearch. """ print("Обновление настроек индексов Meilisearch...") - + try: # Обновляем фильтруемые атрибуты для продуктов print("Обновление фильтруемых атрибутов для продуктов...") client.index(PRODUCT_INDEX).update_filterable_attributes([ - "category_id", "collection_id", "is_active", "price", "discount_price", "slug", "id" + "category_id", "collection_id", "is_active", "price", "discount_price", "slug", "id", "size_ids" ]) - + # Обновляем поисковые атрибуты для продуктов print("Обновление поисковых атрибутов для продуктов...") client.index(PRODUCT_INDEX).update_searchable_attributes([ "name", "description", "slug" ]) - + # Обновляем сортируемые атрибуты для продуктов print("Обновление сортируемых атрибутов для продуктов...") client.index(PRODUCT_INDEX).update_sortable_attributes([ "price", "discount_price", "created_at", "name" ]) - + # Обновляем фильтруемые атрибуты для категорий print("Обновление фильтруемых атрибутов для категорий...") client.index(CATEGORY_INDEX).update_filterable_attributes([ "parent_id", "is_active", "id", "slug" ]) - + # Обновляем поисковые атрибуты для категорий print("Обновление поисковых атрибутов для категорий...") client.index(CATEGORY_INDEX).update_searchable_attributes([ "name", "description", "slug" ]) - + # Обновляем фильтруемые атрибуты для коллекций print("Обновление фильтруемых атрибутов для коллекций...") client.index(COLLECTION_INDEX).update_filterable_attributes([ "is_active", "id", "slug" ]) - + # Обновляем поисковые атрибуты для коллекций print("Обновление поисковых атрибутов для коллекций...") client.index(COLLECTION_INDEX).update_searchable_attributes([ "name", "description", "slug" ]) - + # Обновляем поисковые атрибуты для размеров print("Обновление поисковых атрибутов для размеров...") client.index(SIZE_INDEX).update_searchable_attributes([ "name", "code", "description" ]) - + print("Настройки индексов Meilisearch обновлены успешно!") except Exception as e: print(f"Ошибка при обновлении настроек индексов Meilisearch: {str(e)}") diff --git a/frontend old/.dockerignore b/frontend old/.dockerignore deleted file mode 100644 index c550055..0000000 --- a/frontend old/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -Dockerfile -.dockerignore -node_modules -npm-debug.log -README.md -.next -.git diff --git a/frontend old/.gitignore b/frontend old/.gitignore deleted file mode 100644 index 8777267..0000000 --- a/frontend old/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/frontend old/Dockerfile b/frontend old/Dockerfile deleted file mode 100644 index b70d7e7..0000000 --- a/frontend old/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# syntax=docker.io/docker/dockerfile:1 - -FROM node:18-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ - else echo "Lockfile not found." && exit 1; \ - fi - - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -# ENV NEXT_TELEMETRY_DISABLED=1 - -RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ - else echo "Lockfile not found." && exit 1; \ - fi - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -# Uncomment the following line in case you want to disable telemetry during runtime. -# ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 - -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output -ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] diff --git a/frontend old/README.md b/frontend old/README.md deleted file mode 100644 index d61fc6b..0000000 --- a/frontend old/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# With Docker - -This examples shows how to use Docker with Next.js based on the [deployment documentation](https://nextjs.org/docs/deployment#docker-image). Additionally, it contains instructions for deploying to Google Cloud Run. However, you can use any container-based deployment host. - -## How to use - -Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: - -```bash -npx create-next-app --example with-docker nextjs-docker -``` - -```bash -yarn create next-app --example with-docker nextjs-docker -``` - -```bash -pnpm create next-app --example with-docker nextjs-docker -``` - -## Using Docker - -1. [Install Docker](https://docs.docker.com/get-docker/) on your machine. -1. Build your container: `docker build -t nextjs-docker .`. -1. Run your container: `docker run -p 3000:3000 nextjs-docker`. - -You can view your images created with `docker images`. - -### In existing projects - -To add support for Docker to an existing project, just copy the [`Dockerfile`](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile) into the root of the project and add the following to the `next.config.js` file: - -```js -// next.config.js -module.exports = { - // ... rest of the configuration. - output: "standalone", -}; -``` - -This will build the project as a standalone app inside the Docker image. - -## Deploying to Google Cloud Run - -1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) so you can use `gcloud` on the command line. -1. Run `gcloud auth login` to log in to your account. -1. [Create a new project](https://cloud.google.com/run/docs/quickstarts/build-and-deploy) in Google Cloud Run (e.g. `nextjs-docker`). Ensure billing is turned on. -1. Build your container image using Cloud Build: `gcloud builds submit --tag gcr.io/PROJECT-ID/helloworld --project PROJECT-ID`. This will also enable Cloud Build for your project. -1. Deploy to Cloud Run: `gcloud run deploy --image gcr.io/PROJECT-ID/helloworld --project PROJECT-ID --platform managed --allow-unauthenticated`. Choose a region of your choice. - - - You will be prompted for the service name: press Enter to accept the default name, `helloworld`. - - You will be prompted for [region](https://cloud.google.com/run/docs/quickstarts/build-and-deploy#follow-cloud-run): select the region of your choice, for example `us-central1`. - -## Running Locally - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. diff --git a/frontend old/app.json b/frontend old/app.json deleted file mode 100644 index 5f394f2..0000000 --- a/frontend old/app.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "nextjs", - "options": { - "allow-unauthenticated": true, - "memory": "256Mi", - "cpu": "1", - "port": 3000, - "http2": false - } -} diff --git a/frontend old/components/AddToCartButton.tsx b/frontend old/components/AddToCartButton.tsx deleted file mode 100644 index 59f1ef9..0000000 --- a/frontend old/components/AddToCartButton.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from 'react'; -import { useRouter } from 'next/router'; -import { ShoppingCart, Check, AlertCircle } from 'lucide-react'; -import cartService from '../services/cart'; -import authService from '../services/auth'; - -interface AddToCartButtonProps { - variantId: number; - quantity?: number; - className?: string; - onAddToCart?: () => void; -} - -export default function AddToCartButton({ variantId, quantity = 1, className = '', onAddToCart }: AddToCartButtonProps) { - const router = useRouter(); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - const [error, setError] = useState(null); - - const handleAddToCart = async () => { - // Проверяем, авторизован ли пользователь - if (!authService.isAuthenticated()) { - // Сохраняем текущий URL для редиректа после авторизации - const currentPath = router.asPath; - router.push(`/login?redirect=${encodeURIComponent(currentPath)}`); - return; - } - - setLoading(true); - setError(null); - - try { - await cartService.addToCart({ - variant_id: variantId, - quantity: quantity - }); - - setSuccess(true); - - // Вызываем колбэк, если он передан - if (onAddToCart) { - onAddToCart(); - } - - // Сбрасываем состояние успеха через 2 секунды - setTimeout(() => { - setSuccess(false); - }, 2000); - } catch (err) { - console.error('Ошибка при добавлении в корзину:', err); - setError('Не удалось добавить товар в корзину'); - - // Сбрасываем состояние ошибки через 3 секунды - setTimeout(() => { - setError(null); - }, 3000); - } finally { - setLoading(false); - } - }; - - return ( -
- - - {error && ( -
- - {error} -
- )} -
- ); -} \ No newline at end of file diff --git a/frontend old/components/Collections.tsx b/frontend old/components/Collections.tsx deleted file mode 100644 index 5610390..0000000 --- a/frontend old/components/Collections.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Image from "next/image" -import Link from "next/link" -import { motion } from "framer-motion" -import { Collection } from "../data/collections" - -interface CollectionsProps { - collections: Collection[] -} - -export default function Collections({ collections }: CollectionsProps) { - return ( -
-

Коллекции

- -
- {collections.map((collection, index) => ( - - -
- {collection.name} -
-
-

{collection.name}

-

{collection.description}

- - Смотреть коллекцию - -
-
-
- - ))} -
-
- ) -} \ No newline at end of file diff --git a/frontend old/components/CookieNotification.tsx b/frontend old/components/CookieNotification.tsx deleted file mode 100644 index f0471ae..0000000 --- a/frontend old/components/CookieNotification.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" - -export default function CookieNotification() { - const [isVisible, setIsVisible] = useState(false); - - // Проверяем, было ли уже показано уведомление - useEffect(() => { - const cookieAccepted = localStorage.getItem('cookieAccepted'); - if (!cookieAccepted) { - // Показываем уведомление с небольшой задержкой - const timer = setTimeout(() => { - setIsVisible(true); - }, 2000); - - return () => clearTimeout(timer); - } - }, []); - - const acceptCookies = () => { - localStorage.setItem('cookieAccepted', 'true'); - setIsVisible(false); - }; - - if (!isVisible) return null; - - return ( -
-

Уведомление о Cookies

-

- Наш сайт использует файлы cookie. Продолжая пользоваться сайтом, вы соглашаетесь на использование наших файлов - cookie. -

- -
- ); -} \ No newline at end of file diff --git a/frontend old/components/Footer.tsx b/frontend old/components/Footer.tsx deleted file mode 100644 index 77614f1..0000000 --- a/frontend old/components/Footer.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import Link from "next/link"; -import { Facebook, Instagram, Twitter, Youtube, ChevronDown, ChevronUp } from "lucide-react"; -import { useState } from "react"; - -export default function Footer() { - // Состояния для отображения/скрытия разделов на мобильных устройствах - const [helpOpen, setHelpOpen] = useState(false); - const [shopOpen, setShopOpen] = useState(false); - const [aboutOpen, setAboutOpen] = useState(false); - - return ( - - ); -} \ No newline at end of file diff --git a/frontend old/components/Header.tsx b/frontend old/components/Header.tsx deleted file mode 100644 index c70349e..0000000 --- a/frontend old/components/Header.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import Link from "next/link"; -import { Search, Heart, User, ShoppingCart, ChevronLeft, LogOut, Menu, X } from "lucide-react"; -import { useState, useEffect } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import authService from "../services/auth"; -import cartService from "../services/cart"; - -export default function Header() { - const router = useRouter(); - // Состояние для отслеживания прокрутки страницы - const [scrolled, setScrolled] = useState(false); - // Состояние для отслеживания аутентификации пользователя - const [isAuthenticated, setIsAuthenticated] = useState(false); - // Состояние для отображения выпадающего меню пользователя - const [showUserMenu, setShowUserMenu] = useState(false); - // Состояние для отображения мобильного меню - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - // Состояние для хранения количества товаров в корзине - const [cartItemsCount, setCartItemsCount] = useState(0); - - // Эффект для проверки аутентификации при загрузке компонента - useEffect(() => { - setIsAuthenticated(authService.isAuthenticated()); - }, []); - - // Эффект для загрузки количества товаров в корзине - useEffect(() => { - const fetchCartItemsCount = async () => { - if (authService.isAuthenticated()) { - try { - const cart = await cartService.getCart(); - setCartItemsCount(cart.items_count); - } catch (error) { - console.error('Ошибка при загрузке корзины:', error); - setCartItemsCount(0); - } - } else { - setCartItemsCount(0); - } - }; - - fetchCartItemsCount(); - - // Обновляем количество товаров в корзине при изменении маршрута - const handleRouteChange = () => { - fetchCartItemsCount(); - }; - - router.events.on('routeChangeComplete', handleRouteChange); - return () => { - router.events.off('routeChangeComplete', handleRouteChange); - }; - }, [isAuthenticated, router.events]); - - // Эффект для отслеживания прокрутки - useEffect(() => { - const handleScroll = () => { - const isScrolled = window.scrollY > 50; - if (isScrolled !== scrolled) { - setScrolled(isScrolled); - } - }; - - window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, [scrolled]); - - // Функция для выхода из системы - const handleLogout = () => { - authService.logout(); - setIsAuthenticated(false); - setShowUserMenu(false); - setCartItemsCount(0); - router.push('/'); - }; - - // Функция для возврата на предыдущую страницу - const goBack = () => { - router.back(); - }; - - // Проверяем, находимся ли мы на главной странице - const isHomePage = router.pathname === "/"; - // Проверяем, находимся ли мы на странице категорий или коллекций - const isDetailPage = router.pathname.includes("[slug]"); - - // Функция для переключения отображения меню пользователя - const toggleUserMenu = () => { - setShowUserMenu(!showUserMenu); - }; - - // Функция для переключения мобильного меню - const toggleMobileMenu = () => { - setMobileMenuOpen(!mobileMenuOpen); - }; - - // Закрыть мобильное меню при переходе на другую страницу - useEffect(() => { - setMobileMenuOpen(false); - }, [router.pathname]); - - // Закрыть меню пользователя при клике вне его - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (showUserMenu && !target.closest('.user-menu-container')) { - setShowUserMenu(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [showUserMenu]); - - return ( -
- - - {/* Мобильное меню */} - - {mobileMenuOpen && ( - -
-
- - Каталог - - - Все товары - - - Коллекции - - - Новинки - - - Отследить заказ - - - - Поиск - -
-
-
- )} -
-
- ); -} \ No newline at end of file diff --git a/frontend old/components/Hero.tsx b/frontend old/components/Hero.tsx deleted file mode 100644 index cb9099c..0000000 --- a/frontend old/components/Hero.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client" - -import { useState, useEffect } from "react"; -import Image from "next/image"; -import { motion } from "framer-motion"; - -// Типы для свойств компонента -interface HeroProps { - images?: string[]; -} - -export default function Hero({ images = [] }: HeroProps) { - return ( -
- {/* Логотип по центру */} -
-
- Brand Logo -
-
- - {/* Контент */} -
-
- - Элегантность в каждой детали - - - Откройте для себя новую коллекцию, созданную с любовью к качеству и стилю - - - - Смотреть коллекцию - - -
-
-
- ); -} diff --git a/frontend old/components/NewArrivals.tsx b/frontend old/components/NewArrivals.tsx deleted file mode 100644 index aab4c50..0000000 --- a/frontend old/components/NewArrivals.tsx +++ /dev/null @@ -1,256 +0,0 @@ -"use client" - -import { useState, useRef, useEffect } from "react" -import Image from "next/image" -import { Heart, ChevronLeft, ChevronRight } from "lucide-react" -import Link from "next/link" -import { Product, formatPrice } from "../data/products" -import { motion } from "framer-motion" - -interface NewArrivalsProps { - products: Product[] -} - -export default function NewArrivals({ products }: NewArrivalsProps) { - const [hoveredProduct, setHoveredProduct] = useState(null) - const [favorites, setFavorites] = useState([]) - const sliderRef = useRef(null) - const [showLeftArrow, setShowLeftArrow] = useState(false) - const [showRightArrow, setShowRightArrow] = useState(true) - const [isDragging, setIsDragging] = useState(false) - const [startX, setStartX] = useState(0) - const [scrollLeftValue, setScrollLeftValue] = useState(0) - const [currentSlide, setCurrentSlide] = useState(0) - const [slidesPerView, setSlidesPerView] = useState(4) - - // Функция для добавления/удаления товара из избранного - const toggleFavorite = (id: number, e: React.MouseEvent) => { - e.stopPropagation() - e.preventDefault() - setFavorites((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id])) - } - - // Определение количества слайдов на экране в зависимости от размера экрана - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - if (width < 640) { - setSlidesPerView(1); - } else if (width < 768) { - setSlidesPerView(2); - } else if (width < 1024) { - setSlidesPerView(3); - } else { - setSlidesPerView(4); - } - }; - - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - // Обновление состояния стрелок при скролле - const updateArrowVisibility = () => { - if (sliderRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = sliderRef.current - setShowLeftArrow(scrollLeft > 0) - setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 5) // 5px buffer - - // Обновление текущего слайда - if (clientWidth > 0) { - const slideWidth = scrollWidth / products.length; - const newCurrentSlide = Math.round(scrollLeft / slideWidth); - setCurrentSlide(newCurrentSlide); - } - } - } - - // Инициализация и обработка изменений размера - useEffect(() => { - updateArrowVisibility() - window.addEventListener("resize", updateArrowVisibility) - return () => window.removeEventListener("resize", updateArrowVisibility) - }, []) - - // Обработчики скролла - const handleScroll = () => { - updateArrowVisibility() - } - - const scrollLeft = () => { - if (sliderRef.current) { - const itemWidth = sliderRef.current.scrollWidth / products.length; - const newScrollLeft = Math.max(0, sliderRef.current.scrollLeft - (itemWidth * slidesPerView)); - sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" }); - } - } - - const scrollRight = () => { - if (sliderRef.current) { - const itemWidth = sliderRef.current.scrollWidth / products.length; - const newScrollLeft = Math.min( - sliderRef.current.scrollWidth - sliderRef.current.clientWidth, - sliderRef.current.scrollLeft + (itemWidth * slidesPerView) - ); - sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" }); - } - } - - // Обработчики перетаскивания (для мобильных) - const handleMouseDown = (e: React.MouseEvent) => { - setIsDragging(true) - setStartX(e.pageX - (sliderRef.current?.offsetLeft || 0)) - setScrollLeftValue(sliderRef.current?.scrollLeft || 0) - } - - const handleMouseUp = () => { - setIsDragging(false) - } - - const handleMouseMove = (e: React.MouseEvent) => { - if (!isDragging) return - e.preventDefault() - if (sliderRef.current) { - const x = e.pageX - (sliderRef.current.offsetLeft || 0) - const walk = (x - startX) * 2 // Скорость скролла - sliderRef.current.scrollLeft = scrollLeftValue - walk - } - } - - const handleMouseLeave = () => { - setIsDragging(false) - } - - // Обработчики тач-событий - const handleTouchStart = (e: React.TouchEvent) => { - if (sliderRef.current) { - setStartX(e.touches[0].pageX - (sliderRef.current.offsetLeft || 0)) - setScrollLeftValue(sliderRef.current.scrollLeft) - } - } - - const handleTouchMove = (e: React.TouchEvent) => { - if (sliderRef.current) { - const x = e.touches[0].pageX - (sliderRef.current.offsetLeft || 0) - const walk = (x - startX) * 2 - sliderRef.current.scrollLeft = scrollLeftValue - walk - } - } - - // Переход к определенному слайду - const goToSlide = (index: number) => { - if (sliderRef.current) { - const itemWidth = sliderRef.current.scrollWidth / products.length; - sliderRef.current.scrollTo({ left: itemWidth * index, behavior: "smooth" }); - } - }; - - return ( -
-

Новинки

- - {/* Контейнер слайдера с кнопками навигации */} -
- {/* Кнопка влево */} - {showLeftArrow && ( - - )} - - {/* Слайдер продуктов */} -
- {products.map((product) => ( - - setHoveredProduct(product.id)} - onMouseLeave={() => setHoveredProduct(null)} - > -
-
- 1 ? product.images[1] : product.images[0] - } - alt={product.name} - fill - sizes="(max-width: 640px) 280px, (max-width: 768px) 320px, (max-width: 1024px) 300px, 280px" - className="object-cover transition-all duration-500 hover:scale-105" - /> - {product.isNew && ( - Новинка - )} - -
-
-
-

{product.name}

-

{formatPrice(product.price)} ₽

-
- -
- ))} -
- - {/* Кнопка вправо */} - {showRightArrow && ( - - )} -
- - {/* Индикаторы слайдов (точки) */} -
- {Array.from({ length: Math.ceil(products.length / slidesPerView) }).map((_, index) => ( -
-
- ) -} \ No newline at end of file diff --git a/frontend old/components/PopularCategories.tsx b/frontend old/components/PopularCategories.tsx deleted file mode 100644 index 4503702..0000000 --- a/frontend old/components/PopularCategories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Image from "next/image" -import Link from "next/link" -import { motion } from "framer-motion" -import { Category } from "../data/categories" - -// Типы для свойств компонента -interface PopularCategoriesProps { - categories: Category[] -} - -export default function PopularCategories({ categories }: PopularCategoriesProps) { - return ( -
-

Популярные категории

- -
- {categories.map((category, index) => ( - - -
- {category.name} -
-
-

{category.name}

-
-
-
- - ))} -
-
- ) -} \ No newline at end of file diff --git a/frontend old/components/TabSelector.tsx b/frontend old/components/TabSelector.tsx deleted file mode 100644 index 75d0a0a..0000000 --- a/frontend old/components/TabSelector.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import { useState, useRef, useEffect } from "react" - -const tabs = ["Новинки", "Коллекции", "Популярное"] - -interface TabSelectorProps { - onTabChange: (index: number) => void; -} - -export default function TabSelector({ onTabChange }: TabSelectorProps) { - const [hoveredIndex, setHoveredIndex] = useState(null) - const [activeIndex, setActiveIndex] = useState(0) - const [hoverStyle, setHoverStyle] = useState({}) - const [activeStyle, setActiveStyle] = useState({ left: "0px", width: "0px" }) - const tabRefs = useRef<(HTMLDivElement | null)[]>([]) - - useEffect(() => { - if (hoveredIndex !== null) { - const hoveredElement = tabRefs.current[hoveredIndex] - if (hoveredElement) { - const { offsetLeft, offsetWidth } = hoveredElement - setHoverStyle({ - left: `${offsetLeft}px`, - width: `${offsetWidth}px`, - }) - } - } - }, [hoveredIndex]) - - useEffect(() => { - const activeElement = tabRefs.current[activeIndex] - if (activeElement) { - const { offsetLeft, offsetWidth } = activeElement - setActiveStyle({ - left: `${offsetLeft}px`, - width: `${offsetWidth}px`, - }) - } - - // Вызываем функцию обратного вызова при изменении активного таба - onTabChange(activeIndex); - }, [activeIndex, onTabChange]) - - useEffect(() => { - requestAnimationFrame(() => { - const firstElement = tabRefs.current[0] - if (firstElement) { - const { offsetLeft, offsetWidth } = firstElement - setActiveStyle({ - left: `${offsetLeft}px`, - width: `${offsetWidth}px`, - }) - } - }) - }, []) - - return ( -
-
-
-
- {/* Hover Highlight */} -
- - {/* Active Indicator */} -
- - {/* Tabs */} -
- {tabs.map((tab, index) => ( -
(tabRefs.current[index] = el)} - className={`px-3 py-2 cursor-pointer transition-colors duration-300 h-[30px] ${ - index === activeIndex ? "text-[#2B5F47] font-medium" : "text-[#2B5F47]/70" - }`} - onMouseEnter={() => setHoveredIndex(index)} - onMouseLeave={() => setHoveredIndex(null)} - onClick={() => setActiveIndex(index)} - > -
- {tab} -
-
- ))} -
-
-
-
-
- ) -} \ No newline at end of file diff --git a/frontend old/components/admin/AdminLayout.tsx b/frontend old/components/admin/AdminLayout.tsx deleted file mode 100644 index ef36443..0000000 --- a/frontend old/components/admin/AdminLayout.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { useState, useEffect } from 'react'; -import Head from 'next/head'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { - LayoutDashboard, - Package, - Tag, - ShoppingBag, - Users, - FileText, - Settings, - LogOut, - Menu, - X, - Home, - Grid -} from 'lucide-react'; -import Image from 'next/image'; -import authService from '../../services/auth'; -import { userService } from '../../services/users'; - -// Компонент элемента бокового меню -interface SidebarItemProps { - icon: React.ReactNode; - text: string; - href: string; - active: boolean; -} - -const SidebarItem = ({ icon, text, href, active }: SidebarItemProps) => { - return ( - - {icon} - {text} - - ); -}; - -interface AdminLayoutProps { - children: React.ReactNode; - title: string; -} - -export default function AdminLayout({ children, title }: AdminLayoutProps) { - const router = useRouter(); - const [sidebarOpen, setSidebarOpen] = useState(false); - const [loading, setLoading] = useState(true); - - // Проверка прав администратора при загрузке компонента - useEffect(() => { - const checkAdminAccess = async () => { - try { - if (!authService.isAuthenticated()) { - // Если пользователь не авторизован, перенаправляем на страницу входа - router.push('/login'); - return; - } - - const user = await userService.getCurrentUser(); - - if (!user) { - // Если не удалось получить данные пользователя, перенаправляем на страницу входа - router.push('/login'); - return; - } - - if (!user.is_admin) { - // Если пользователь не администратор, перенаправляем на главную страницу - router.push('/'); - return; - } - - setLoading(false); - } catch (error) { - console.error('Ошибка при проверке прав администратора:', error); - router.push('/login'); - } - }; - - checkAdminAccess(); - }, [router]); - - // Определяем активный пункт меню на основе текущего пути - const currentPath = router.pathname; - - const menuItems = [ - { - icon: , - text: 'Панель управления', - href: '/admin', - active: currentPath === '/admin' - }, - { - icon: , - text: 'Товары', - href: '/admin/products', - active: currentPath.startsWith('/admin/products') - }, - { - icon: , - text: 'Категории', - href: '/admin/categories', - active: currentPath.startsWith('/admin/categories') - }, - { - icon: , - text: 'Заказы', - href: '/admin/orders', - active: currentPath.startsWith('/admin/orders') - }, - { - icon: , - text: 'Пользователи', - href: '/admin/users', - active: currentPath.startsWith('/admin/users') - }, - { - icon: , - text: 'Настройки', - href: '/admin/settings', - active: currentPath.startsWith('/admin/settings') - }, - { - icon: , - text: 'Коллекции', - href: '/admin/collections', - active: currentPath.startsWith('/admin/collections') - } - ]; - - if (loading) { - return ( -
-
-
- ); - } - - return ( -
- - {title} | Админ-панель - - - {/* Мобильная навигация */} -
-
- -
-
- Brand Logo -
- Админ -
-
-
- - {/* Боковое меню (мобильное) */} - {sidebarOpen && ( -
-
setSidebarOpen(false)}>
-
-
-
-
- Brand Logo -
- Админ -
- -
-
- -
-
- - - На главную - - -
-
-
- )} - - {/* Боковое меню (десктоп) */} -
-
-
-
- Brand Logo -
- Админ -
-
-
- -
-
- - - На главную - - -
-
- - {/* Основной контент */} -
-
-

{title}

-
-
- {children} -
-
- © {new Date().getFullYear()} Dressed for Success. Все права защищены. -
-
-
- ); -} \ No newline at end of file diff --git a/frontend old/components/admin/AdminSidebar.tsx b/frontend old/components/admin/AdminSidebar.tsx deleted file mode 100644 index c5bac14..0000000 --- a/frontend old/components/admin/AdminSidebar.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { - LayoutDashboard, - ShoppingBag, - Tag, - FileText, - Settings, - Users, - ShoppingCart, - BarChart, - MessageSquare -} from 'lucide-react'; - -// Определение элементов меню -const menuItems = [ - { - title: 'Дашборд', - icon: , - href: '/admin/dashboard', - active: (path) => path === '/admin/dashboard' - }, - { - title: 'Заказы', - icon: , - href: '/admin/orders', - active: (path) => path.startsWith('/admin/orders') - }, - { - title: 'Клиенты', - icon: , - href: '/admin/customers', - active: (path) => path.startsWith('/admin/customers') - }, - { - title: 'Категории', - icon: , - href: '/admin/categories', - active: (path) => path.startsWith('/admin/categories') - }, - { - title: 'Товары', - icon: , - href: '/admin/products', - active: (path) => path.startsWith('/admin/products') - }, - { - title: 'Страницы', - icon: , - href: '/admin/pages', - active: (path) => path.startsWith('/admin/pages') - }, - { - title: 'Отзывы', - icon: , - href: '/admin/reviews', - active: (path) => path.startsWith('/admin/reviews') - }, - { - title: 'Аналитика', - icon: , - href: '/admin/analytics', - active: (path) => path.startsWith('/admin/analytics') - }, - { - title: 'Настройки', - icon: , - href: '/admin/settings', - active: (path) => path.startsWith('/admin/settings') - } -]; - -export default function AdminSidebar() { - const router = useRouter(); - const currentPath = router.pathname; - - return ( -
-
- - DressedForSuccess - -
- -
- ); -} \ No newline at end of file diff --git a/frontend old/components/admin/ProductForm.tsx b/frontend old/components/admin/ProductForm.tsx deleted file mode 100644 index ded27f9..0000000 --- a/frontend old/components/admin/ProductForm.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import { Save, X } from 'lucide-react'; -import { Product, ProductVariant, Category, Collection, ProductImage } from '../../services/catalog'; -import { productService, categoryService, collectionService } from '../../services/catalog'; -import ProductImageUploader, { ProductImageWithFile } from './ProductImageUploader'; -import ProductVariantManager from './ProductVariantManager'; - -interface ProductFormProps { - product?: Product; - onSuccess?: () => void; -} - -const ProductForm: React.FC = ({ product, onSuccess }) => { - const router = useRouter(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [images, setImages] = useState([]); - const [variants, setVariants] = useState([]); - const [categories, setCategories] = useState([]); - const [collections, setCollections] = useState([]); - const [formData, setFormData] = useState({ - name: '', - slug: '', - description: '', - category_id: '', - collection_id: '', - is_active: true - }); - const [autoGenerateSlug, setAutoGenerateSlug] = useState(true); - - // Загрузка категорий и коллекций при монтировании компонента - useEffect(() => { - const fetchData = async () => { - try { - const [categoriesData, collectionsData] = await Promise.all([ - categoryService.getCategories(), - collectionService.getCollections() - ]); - setCategories(categoriesData); - setCollections(collectionsData); - } catch (err) { - console.error('Ошибка при загрузке данных:', err); - setError('Не удалось загрузить данные категорий и коллекций.'); - } - }; - - fetchData(); - - // Если передан продукт, заполняем форму его данными - if (product) { - setFormData({ - name: product.name || '', - slug: product.slug || '', - description: product.description || '', - category_id: product.category_id ? String(product.category_id) : '', - collection_id: product.collection_id ? String(product.collection_id) : '', - is_active: typeof product.is_active === 'boolean' ? product.is_active : true - }); - setAutoGenerateSlug(false); - - // Загружаем изображения - if (product.images && Array.isArray(product.images)) { - const productImages: ProductImageWithFile[] = product.images.map(img => ({ - id: img.id, - url: img.url, - is_primary: img.is_primary, - product_id: img.product_id - })); - setImages(productImages); - } - - // Загружаем варианты - if (product.variants && Array.isArray(product.variants)) { - setVariants(product.variants); - } - } - }, [product]); - - // Автоматическая генерация slug при изменении названия товара - useEffect(() => { - if (autoGenerateSlug && formData.name) { - setFormData(prev => ({ - ...prev, - slug: generateSlug(formData.name) - })); - } - }, [formData.name, autoGenerateSlug]); - - const handleChange = (e) => { - const { name, value, type, checked } = e.target; - setFormData({ - ...formData, - [name]: type === 'checkbox' ? checked : value - }); - - // Если пользователь изменяет slug вручную, отключаем автогенерацию - if (name === 'slug') { - setAutoGenerateSlug(false); - } - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!formData.name || !formData.category_id || variants.length === 0) { - setError('Пожалуйста, заполните все обязательные поля и добавьте хотя бы один вариант товара.'); - return; - } - - try { - setLoading(true); - setError(''); - - // Используем введенный slug или генерируем новый, если поле пустое - const slug = formData.slug || generateSlug(formData.name); - - // Подготавливаем данные продукта - const productData = { - name: formData.name, - slug, - description: formData.description, - category_id: parseInt(formData.category_id, 10), - collection_id: formData.collection_id ? parseInt(formData.collection_id, 10) : null, - is_active: formData.is_active - }; - - console.log('Отправляемые данные продукта:', productData); - - let productId: number; - - if (product) { - // Обновляем существующий продукт - console.log(`Обновление продукта с ID: ${product.id}`); - const updatedProduct = await productService.updateProduct(product.id, productData); - console.log('Обновленный продукт:', updatedProduct); - productId = updatedProduct.id; - } else { - // Создаем новый продукт - console.log('Создание нового продукта'); - const newProduct = await productService.createProduct(productData); - console.log('Созданный продукт:', newProduct); - productId = newProduct.id; - } - - // Загружаем изображения - console.log(`Загрузка ${images.length} изображений для продукта ${productId}`); - - // Используем Promise.all для параллельной загрузки изображений - const imagePromises = images.map(async (image) => { - try { - if (image.file) { - // Загружаем новое изображение - console.log(`Загрузка нового изображения: ${image.file.name}`); - const uploadedImage = await productService.uploadProductImage(productId, image.file, image.is_primary); - console.log('Загруженное изображение:', uploadedImage); - return uploadedImage; - } else if (image.id && product) { - // Обновляем существующее изображение - console.log(`Обновление существующего изображения с ID: ${image.id}`); - const updatedImage = await productService.updateProductImage(productId, image.id, { is_primary: image.is_primary }); - console.log('Обновленное изображение:', updatedImage); - return updatedImage; - } - return null; - } catch (imgError) { - console.error('Ошибка при обработке изображения:', imgError); - if (imgError.response) { - console.error('Данные ответа:', imgError.response.data); - console.error('Статус ответа:', imgError.response.status); - } - return null; - } - }); - - const uploadedImages = await Promise.all(imagePromises); - console.log('Все изображения обработаны:', uploadedImages.filter(Boolean)); - - // Обрабатываем варианты товара - if (product) { - // Для существующего продукта варианты уже обработаны через API в компоненте ProductVariantManager - console.log('Варианты для существующего продукта уже обработаны'); - } else { - // Для нового продукта создаем варианты - console.log(`Создание ${variants.length} вариантов для нового продукта ${productId}`); - - const variantPromises = variants.map(async (variant) => { - try { - console.log('Отправка данных варианта:', variant); - // Явно указываем все поля, чтобы избежать лишних данных - const variantData = { - name: variant.name, - sku: variant.sku, - price: Number(variant.price), // Убедимся, что это число - discount_price: variant.discount_price ? Number(variant.discount_price) : null, - stock: Number(variant.stock), - is_active: Boolean(variant.is_active || true) - }; - - const newVariant = await productService.addProductVariant(productId, variantData); - console.log('Созданный вариант:', newVariant); - return newVariant; - } catch (varError) { - console.error('Ошибка при создании варианта:', varError); - if (varError.response) { - console.error('Ответ сервера:', varError.response.data); - console.error('Статус ответа:', varError.response.status); - } - return null; - } - }); - - const createdVariants = await Promise.all(variantPromises); - console.log('Все варианты обработаны:', createdVariants.filter(Boolean)); - } - - // Вызываем колбэк успешного завершения или перенаправляем на страницу списка товаров - if (onSuccess) { - onSuccess(); - } else { - router.push('/admin/products'); - } - } catch (err) { - console.error('Ошибка при сохранении товара:', err); - if (err.response) { - console.error('Ответ сервера:', err.response.data); - console.error('Статус ответа:', err.response.status); - } - setError('Не удалось сохранить товар. Пожалуйста, проверьте введенные данные и попробуйте снова.'); - } finally { - setLoading(false); - } - }; - - const generateSlug = (name) => { - return name - .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/[\s_-]+/g, '-') - .replace(/^-+|-+$/g, ''); - }; - - return ( -
-
- {error && ( -
- Ошибка! - {error} -
- )} - -
-
- - -
- -
- -
- -
-
- -
-
- -
- - -
- -
- - -
- -
- -
-
- -
- -