diff --git a/.DS_Store b/.DS_Store index b38128b..b7db6ae 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..51b1b71 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# Общие файлы +.git +.github +.gitignore +.DS_Store +README.md +**/test +**/.env + +# Файлы и директории для Node.js +**/node_modules +**/npm-debug.log +**/.next +**/dist +**/build +**/.coverage +**/coverage + +# Файлы и директории для Python +**/__pycache__ +**/*.py[cod] +**/*.so +**/*.egg +**/*.egg-info +**/dist +**/build +**/.pytest_cache +**/.coverage +**/htmlcov +**/.tox +**/.mypy_cache + +# Виртуальное окружение Python +**/venv +**/.venv +**/env +**/.env + +# Файлы базы данных +**/*.sqlite +**/*.db + +# Логи и временные файлы +**/logs +**/*.log +**/tmp +**/*.tmp + +# Файлы IDE +**/.idea +**/.vscode +**/*.swp +**/*.swo + +# Загружаемые файлы, кроме необходимых для работы +# backend/uploads/* +# !backend/uploads/products +# backend/uploads/products/* +# !backend/uploads/products/.gitkeep \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..f28d00b --- /dev/null +++ b/.env.production @@ -0,0 +1,17 @@ +# Доменное имя сайта без протокола (http/https) +DOMAIN_NAME=dressedforsuccess.shop + +# Протокол (http или https) +PROTOCOL=https + +# API URL для браузера (клиентская сторона) +NEXT_PUBLIC_API_URL=${PROTOCOL}://${DOMAIN_NAME}/api + +# Base URL для статических файлов +NEXT_PUBLIC_BASE_URL=${PROTOCOL}://${DOMAIN_NAME} + +# Режим отладки +NEXT_PUBLIC_DEBUG=false + +# Мокирование API +NEXT_PUBLIC_MOCK_API=false \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..4586976 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,28 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Установка зависимостей системы +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Установка зависимостей Python +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Копирование кода приложения +COPY backend/ . + +# Копирование .env.docker в .env для использования в контейнере +COPY backend/.env.docker ./app/.env + +# Создание директории для загрузок если её нет +RUN mkdir -p /app/uploads/products + +# Настройка разрешений для директории загрузок +RUN chmod -R 777 /app/uploads + +# Открытие порта +EXPOSE 8000 + +# Запуск приложения с Uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..5984ed1 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,24 @@ +FROM node:20-alpine + +WORKDIR /app + +# Копирование файлов package.json и package-lock.json +COPY frontend/package*.json ./ + +# Установка зависимостей с флагом --legacy-peer-deps +RUN npm ci --legacy-peer-deps + +# Копирование исходного кода +COPY frontend/ ./ + +# Копирование .env.docker в .env.local для использования в контейнере +COPY frontend/.env.docker ./.env.local + +# Сборка приложения +RUN npm run build + +# Открытие порта +EXPOSE 3000 + +# Запуск приложения +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fcc0de --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Dressed for Success - Интернет-магазин одежды + +## О проекте + +Интернет-магазин модной одежды Dressed for Success, созданный с использованием современных технологий: + +- **Фронтенд**: Next.js, React, TypeScript, Tailwind CSS +- **Бэкенд**: FastAPI, SQLAlchemy, PostgreSQL +- **Развертывание**: Docker, Docker Compose + +## Требования + +Для запуска проекта вам потребуются: + +- Docker +- Docker Compose + +## Запуск проекта + +### 1. Клонирование репозитория + +```bash +git clone https://github.com/username/dressed_for_success_store.git +cd dressed_for_success_store +``` + +### 2. Запуск через Docker Compose + +```bash +docker-compose up -d +``` + +Это запустит: +- Бэкенд на порту 8000 +- Фронтенд на порту 3000 +- PostgreSQL на порту 5432 + +### 3. Доступ к приложению + +- **Фронтенд**: http://localhost:3000 +- **API бэкенда**: http://localhost:8000/api +- **Документация API**: http://localhost:8000/docs + +## Остановка проекта + +```bash +docker-compose down +``` + +Для удаления томов (данных базы данных и загруженных файлов): + +```bash +docker-compose down -v +``` + +## Разработка + +### Структура проекта + +``` +. +├── backend/ # Бэкенд на FastAPI +│ ├── app/ # Код приложения +│ ├── uploads/ # Загружаемые файлы +│ └── requirements.txt # Зависимости Python +│ +├── frontend/ # Фронтенд на Next.js +│ ├── app/ # Код Next.js приложения +│ ├── components/ # React компоненты +│ ├── lib/ # Библиотеки и утилиты +│ └── public/ # Статические файлы +│ +├── docker-compose.yml # Конфигурация Docker Compose +├── Dockerfile.backend # Dockerfile для бэкенда +└── Dockerfile.frontend # Dockerfile для фронтенда +``` + +## Переменные окружения + +### Фронтенд + +Основные переменные окружения для фронтенда (файл `.env.local` или `.env.docker`): + +``` +NEXT_PUBLIC_API_URL=http://localhost:8000/api +NEXT_PUBLIC_BASE_URL=http://localhost:8000 +NEXT_PUBLIC_DEBUG=false +NEXT_PUBLIC_MOCK_API=false +``` + +### Бэкенд + +Основные переменные окружения для бэкенда (файл `.env` или `.env.docker`): + +``` +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db +SECRET_KEY=supersecretkey +DEBUG=0 +UPLOAD_DIRECTORY=/app/uploads +``` + +## Лицензия + +[MIT License](LICENSE) \ No newline at end of file diff --git a/backend/.env.docker b/backend/.env.docker new file mode 100644 index 0000000..76e4300 --- /dev/null +++ b/backend/.env.docker @@ -0,0 +1,10 @@ +# Настройки базы данных +# DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db + +# Настройки приложения +DEBUG=0 +SECRET_KEY=supersecretkey +# UPLOAD_DIRECTORY=/app/uploads + +# Настройки CORS +FRONTEND_URL=http://frontend:3000 \ No newline at end of file diff --git a/backend/app.db b/backend/app.db deleted file mode 100644 index 819cd93..0000000 Binary files a/backend/app.db and /dev/null differ diff --git a/backend/app/__pycache__/config.cpython-310.pyc b/backend/app/__pycache__/config.cpython-310.pyc index f7b0c70..38f76e3 100644 Binary files a/backend/app/__pycache__/config.cpython-310.pyc and b/backend/app/__pycache__/config.cpython-310.pyc differ diff --git a/backend/app/config.py b/backend/app/config.py index afa5f82..316008c 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -28,7 +28,8 @@ MAIL_TLS = True MAIL_SSL = False # Настройки загрузки файлов -UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "uploads") +# UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "uploads") +UPLOAD_DIRECTORY = "/" ALLOWED_UPLOAD_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"} MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 МБ @@ -55,7 +56,7 @@ class Settings(BaseSettings): APP_DESCRIPTION: str = "API для интернет-магазина на FastAPI" # Настройки базы данных - DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5434/shop_db") + DATABASE_URL: str = "postgresql://gen_user:F%2BgEEiP3h7yB6d@93.183.81.86:5432/shop_db" # Настройки безопасности SECRET_KEY: str = SECRET_KEY diff --git a/backend/requirements.txt b/backend/requirements.txt index c188be1..fc8751d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,13 +1,108 @@ -fastapi==0.105.0 -uvicorn==0.24.0 -sqlalchemy==2.0.23 -pydantic==2.5.2 + +aiofiles==24.1.0 +aiogram==3.17.0 +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiosignal==1.3.2 +amqp==5.3.1 +annotated-types==0.6.0 +anthropic==0.45.2 +anyio==4.2.0 +argcomplete==1.10.3 +asyncpg==0.30.0 +attrs==25.1.0 +billiard==4.2.1 +Brotli==1.1.0 +certifi==2025.1.31 +chardet==3.0.4 +click==8.1.7 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 +compressed_rtf==1.0.6 +distro==1.9.0 +docx2txt==0.8 +ebcdic==1.1.1 +et_xmlfile==2.0.0 +extract-msg==0.28.7 +fastapi==0.109.0 +frozenlist==1.5.0 +greenlet==3.0.3 +groq==0.16.0 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.1 +idna==3.6 +IMAPClient==2.1.0 +importlib_metadata==8.6.1 +inflate64==1.0.1 +jiter==0.8.2 +kombu==5.4.2 +lxml==5.3.0 +magic-filter==1.0.12 +multidict==6.1.0 +multivolumefile==0.2.3 +numpy==2.2.2 +olefile==0.47 +openai==1.61.0 +openpyxl==3.1.5 +outcome==1.3.0.post0 +packaging==24.2 +pandas==2.2.3 +patool==3.1.0 +pdfminer.six==20191110 +pillow==11.1.0 +prompt_toolkit==3.0.50 +propcache==0.2.1 +psutil==6.1.1 +psycopg2-binary==2.9.9 +py7zr==0.22.0 +pybcj==1.0.3 +pycron==3.1.2 +pycryptodome==3.21.0 +pycryptodomex==3.21.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 +pydantic_core==2.14.6 +pyppmd==1.1.1 +PySocks==1.7.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.0 +python-pptx==0.6.23 +pytz==2025.1 +pyzstd==0.16.2 +rarfile==4.2 +six==1.17.0 +sniffio==1.3.0 +sortedcontainers==2.4.0 +soupsieve==2.6 +SQLAlchemy==2.0.25 +starlette==0.35.1 +taskiq==0.11.10 +taskiq-dependencies==1.5.6 +taskiq-redis==1.0.2 +tenacity==9.0.0 +texttable==1.7.0 +tqdm==4.67.1 +trio==0.28.0 +trio-websocket==0.11.1 +typing_extensions==4.12.2 +tzdata==2025.1 +tzlocal==5.2 +urllib3==2.3.0 +uvicorn==0.34.0 +vine==5.1.0 +wcwidth==0.2.13 +websocket-client==1.8.0 +wsproto==1.2.0 +xlrd==1.2.0 +XlsxWriter==3.2.2 +yarl==1.18.3 +zipp==3.21.0 python-jose==3.3.0 passlib==1.7.4 +bcrypt<4.0.0 python-multipart==0.0.6 email-validator==2.1.0 -psycopg2-binary==2.9.6 -alembic==1.12.1 -python-dotenv==1.0.0 -bcrypt==4.0.1 -httpx==0.25.2 \ No newline at end of file +setuptools==75.8.0 +cachetools diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e26b10e --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,114 @@ +version: '3.8' +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: dressed-for-success-backend + hostname: backend + expose: + - "8000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db + - DEBUG=0 + - SECRET_KEY=${SECRET_KEY:-supersecretkey} + - UPLOAD_DIRECTORY=/app/uploads + depends_on: + postgres: + condition: service_healthy + volumes: + - ./backend/uploads:/app/uploads + networks: + app_network: + aliases: + - backend + restart: always + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8000/" ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 15s + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: dressed-for-success-frontend + hostname: frontend + expose: + - "3000" + environment: + - NEXT_PUBLIC_API_URL=https://${DOMAIN_NAME}/api + - NEXT_PUBLIC_BASE_URL=https://${DOMAIN_NAME} + - NODE_ENV=production + depends_on: + backend: + condition: service_healthy + networks: + app_network: + aliases: + - frontend + restart: always + + nginx: + image: nginx:alpine + container_name: dressed-for-success-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro + - ./backend/uploads:/app/uploads:ro + - ./certbot/conf:/etc/letsencrypt:ro + - ./certbot/www:/var/www/certbot:ro + depends_on: + - frontend + - backend + networks: + - app_network + restart: always + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + + certbot: + image: certbot/certbot + container_name: dressed-for-success-certbot + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + networks: + - app_network + depends_on: + - nginx + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + + postgres: + image: postgres:15 + container_name: dressed-for-success-db + hostname: postgres + environment: + POSTGRES_DB: shop_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + expose: + - "5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + networks: + app_network: + aliases: + - postgres + restart: always + +networks: + app_network: + driver: bridge + +volumes: + postgres_data: + driver: local \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4b2bcab..93e1b1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,77 +1,78 @@ - version: '3.8' services: - # backend: - # build: - # context: ./backend - # dockerfile: Dockerfile - # container_name: backend - # expose: - # - "8000" - # restart: always + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: dressed-for-success-backend + hostname: backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db + - DEBUG=0 + - SECRET_KEY=supersecretkey + - UPLOAD_DIRECTORY=/app/uploads + depends_on: + postgres: + condition: service_healthy + volumes: + - ./backend/uploads:/app/uploads + networks: + app_network: + aliases: + - backend + dns_search: . + restart: always + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8000/" ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 15s - # frontend: - # build: - # context: ./frontend - # dockerfile: Dockerfile - # container_name: frontend - # expose: - # - "3000" - # environment: - # - NODE_ENV=production - # restart: always - # networks: - # - sta_network + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: dressed-for-success-frontend + hostname: frontend + expose: + - "3000" + environment: + - NEXT_PUBLIC_API_URL=http://0.0.0.0:8000/api + - NEXT_PUBLIC_BASE_URL=http://0.0.0.0:8000 + - NODE_ENV=production + depends_on: + backend: + condition: service_healthy + networks: + app_network: + aliases: + - frontend + dns_search: . + restart: always - # nginx: - # image: nginx:latest - # container_name: nginx - # ports: - # - "80:80" - # volumes: - # - ./nginx/sta_test.conf:/etc/nginx/conf.d/sta_test.conf:ro - # depends_on: - # - backend - # - frontend - # restart: always - # networks: - # - sta_network - - - # backend: - # build: - # context: ./backend - # dockerfile: Dockerfile - # container_name: backend - # ports: - # - "8000:8000" - # expose: - # - "8000" - # environment: - # - DEBUG=1 - # - DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/sta - # - IS_DOCKER=1 - # depends_on: - # postgres: - # condition: service_healthy - # redis: - # condition: service_healthy - # networks: - # - sta_network - # # dns: - # # - 8.8.8.8 - # # - 8.8.4.4 - # # dns_opt: - # # - ndots:1 - # # - timeout:3 - # # - attempts:5 - # # sysctls: - # # - net.ipv4.tcp_keepalive_time=60 - # # - net.ipv4.tcp_keepalive_intvl=10 - # # - net.ipv4.tcp_keepalive_probes=6 + nginx: + image: nginx:alpine + container_name: dressed-for-success-nginx + ports: + - "80:80" + # - "443:443" # Раскомментируйте для HTTPS + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./backend/uploads:/app/uploads:ro + depends_on: + - frontend + - backend + networks: + - app_network + restart: always postgres: image: postgres:15 + container_name: dressed-for-success-db + hostname: postgres environment: POSTGRES_DB: shop_db POSTGRES_USER: postgres @@ -85,45 +86,19 @@ services: interval: 5s timeout: 5s retries: 5 + networks: + app_network: + aliases: + - postgres + dns_search: . + restart: always - - # redis: - # image: redis:7 - # ports: - # - "6380:6379" - # healthcheck: - # test: ["CMD", "redis-cli", "ping"] - # interval: 5s - # timeout: 5s - # retries: 5 - # networks: - # - sta_network - - # elasticsearch: - # image: elasticsearch:8.17.2 - # environment: - # - discovery.type=single-node - # - xpack.security.enabled=false - # - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - # ports: - # - "9200:9200" - # volumes: - # - elasticsearch_data:/usr/share/elasticsearch/data - # healthcheck: - # test: ["CMD", "curl", "-f", "http://localhost:9200"] - # interval: 10s - # timeout: 5s - # retries: 5 - # networks: - # - sta_network - -# networks: -# sta_network: -# name: sta_network -# driver: bridge +networks: + app_network: + driver: bridge volumes: postgres_data: driver: local - # elasticsearch_data: - # driver: local \ No newline at end of file + backend_uploads: + driver: local \ No newline at end of file diff --git a/frontend/.DS_Store b/frontend/.DS_Store index 638ed51..c380920 100644 Binary files a/frontend/.DS_Store and b/frontend/.DS_Store differ diff --git a/frontend/app/(main)/.DS_Store b/frontend/app/(main)/.DS_Store index c3f38e6..2a0b9c6 100644 Binary files a/frontend/app/(main)/.DS_Store and b/frontend/app/(main)/.DS_Store differ diff --git a/frontend/app/(main)/catalog/[slug]/page.tsx b/frontend/app/(main)/catalog/[slug]/page.tsx index ce7c412..613c314 100644 --- a/frontend/app/(main)/catalog/[slug]/page.tsx +++ b/frontend/app/(main)/catalog/[slug]/page.tsx @@ -3,7 +3,7 @@ import { Suspense, useState } from "react" import Link from "next/link" import { notFound } from "next/navigation" -import { ArrowLeft, ChevronRight, Truck, RotateCcw, Heart, ShoppingBag, Mail, Phone, MapPin } from "lucide-react" +import { ArrowLeft, ChevronRight, Truck, RotateCcw, Heart } from "lucide-react" import catalogService, { ProductDetails } from "@/lib/catalog" import { formatPrice } from "@/lib/utils" import { Separator } from "@/components/ui/separator" @@ -77,98 +77,111 @@ export default function ProductPage({ params }: ProductPageProps) { } return ( - <> -
-
- {/* Навигация */} - -
- - - Вернуться в каталог - - - {/* Хлебные крошки */} - - - Главная - - - - Каталог - - {product.category_name && ( - <> - - - {product.category_name} - - - )} - - {product.name} - -
-
- - {/* Основной контент товара */} -
- {/* Блок изображений */} - +
+ {/* Навигация */} + +
+ - }> -
-
- {/* Используем проверку времени создания для выявления новинок (товары, созданные в течение последних 30 дней) */} - {new Date(product.created_at).getTime() > Date.now() - 30 * 24 * 60 * 60 * 1000 && ( - - Новинка - - )} - {product.discount_price && ( - - -{Math.round((1 - product.discount_price / product.price) * 100)}% - - )} -
- - -
-
- + + Вернуться в каталог + - {/* Блок информации о товаре */} - + + Главная + + + + Каталог + + {product.category_name && ( + <> + + + {product.category_name} + + + )} + + {product.name} + +
+
+ + {/* Основной контент товара */} +
+ {/* Блок изображений */} + + }> +
+
+ {/* Используем проверку времени создания для выявления новинок (товары, созданные в течение последних 30 дней) */} + {new Date(product.created_at).getTime() > Date.now() - 30 * 24 * 60 * 60 * 1000 && ( + + Новинка + + )} + {product.discount_price && ( + + -{Math.round((1 - product.discount_price / product.price) * 100)}% + + )} +
+ + {/* Кнопка добавления в избранное */} +
+ +
+ + +
+
+
+ + {/* Блок информации о товаре */} + +
{/* Категория и название */} - + {product.description ? ( @@ -273,9 +286,9 @@ export default function ProductPage({ params }: ProductPageProps) { transition={{ duration: 0.5, delay: 0.6 }} className="grid grid-cols-1 sm:grid-cols-2 gap-4" > -
+
-
+

Доставка

@@ -285,9 +298,9 @@ export default function ProductPage({ params }: ProductPageProps) {

-
+
-
+

Возврат

@@ -297,20 +310,17 @@ export default function ProductPage({ params }: ProductPageProps) {

- -
+
+
-
- - - - + + ) } function ProductSkeleton() { return ( -
+
@@ -319,43 +329,45 @@ function ProductSkeleton() {
- +
-
-
- - - -
- - - -
- -
- {[1, 2, 3, 4].map((i) => ( - - ))} +
+ +
+ + +
- - -
- - - -
-
- - + + + +
+ +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+ +
- -
- -
- - -
+ + + +
+
+ + +
+ +
+ +
+ + +
+
diff --git a/frontend/app/(main)/checkout/success/page.tsx b/frontend/app/(main)/checkout/success/page.tsx index d7f1025..ad25010 100644 --- a/frontend/app/(main)/checkout/success/page.tsx +++ b/frontend/app/(main)/checkout/success/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import Image from "next/image"; -import { useEffect, useState } from "react"; +import { useEffect, useState, Suspense } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Check, ChevronLeft, Package } from "lucide-react"; @@ -10,7 +10,8 @@ import { formatPrice } from "@/lib/utils"; import { OrderItem } from "@/types/order"; import { Separator } from "@/components/ui/separator"; -export default function CheckoutSuccessPage() { +// Компонент с доступом к параметрам URL +function CheckoutSuccessContent() { const router = useRouter(); const searchParams = useSearchParams(); const [orderInfo, setOrderInfo] = useState({ @@ -164,4 +165,30 @@ export default function CheckoutSuccessPage() {
); +} + +// Компонент загрузки для Suspense +function CheckoutSuccessLoading() { + return ( +
+
+
+

+

+
+
+
+
+
+
+ ); +} + +// Основной компонент страницы +export default function CheckoutSuccessPage() { + return ( + }> + + + ); } \ No newline at end of file diff --git a/frontend/app/.DS_Store b/frontend/app/.DS_Store index a5c929b..23a9165 100644 Binary files a/frontend/app/.DS_Store and b/frontend/app/.DS_Store differ diff --git a/frontend/app/admin/dashboard/page.tsx b/frontend/app/admin/dashboard/page.tsx index bd46b43..d4ae516 100644 --- a/frontend/app/admin/dashboard/page.tsx +++ b/frontend/app/admin/dashboard/page.tsx @@ -3,7 +3,9 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { BarChart3, Package, Tag, Users, ShoppingBag } from 'lucide-react'; -import { fetchDashboardStats, fetchRecentOrders, fetchPopularProducts, Order, Product } from '@/lib/api'; +import { fetchDashboardStats, fetchRecentOrders, fetchPopularProducts } from '@/lib/admin-api'; +import { Order } from '@/lib/orders'; +import { Product } from '@/lib/catalog'; // Компонент статистической карточки interface StatCardProps { diff --git a/frontend/app/cart-backup/page.tsx b/frontend/app/cart-backup/page.tsx deleted file mode 100644 index 5615695..0000000 --- a/frontend/app/cart-backup/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client"; - -import React from 'react'; -import { useCart } from '@/hooks/useCart'; -import { CartItem } from '@/components/cart/CartItem'; -import { CartSummary } from '@/components/cart/CartSummary'; -import { EmptyCart } from '@/components/cart/EmptyCart'; -import { Button } from '@/components/ui/button'; -import { Trash2, ArrowLeft, Loader2 } from 'lucide-react'; -import Link from 'next/link'; - -export default function CartPage() { - const { - cart, - loading, - error, - updateCartItem, - removeFromCart, - clearCart - } = useCart(); - - const hasItems = cart.items.length > 0; - - const handleUpdateQuantity = async (id: number, quantity: number) => { - await updateCartItem(id, quantity); - }; - - const handleRemoveItem = async (id: number) => { - await removeFromCart(id); - }; - - const handleClearCart = async () => { - await clearCart(); - }; - - if (loading) { - return ( -
- -

Загрузка корзины...

-
- ); - } - - if (error) { - return ( -
-

{error}

- -
- ); - } - - if (!hasItems) { - return ; - } - - return ( -
-
-
-

Корзина

- -
- -
-
- {cart.items.map((item) => ( - - ))} - -
- - - Продолжить покупки - -
-
- -
- -
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/app/checkout-backup/page.tsx b/frontend/app/checkout-backup/page.tsx deleted file mode 100644 index 0f11992..0000000 --- a/frontend/app/checkout-backup/page.tsx +++ /dev/null @@ -1,205 +0,0 @@ -"use client"; - -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { useToast } from '@/components/ui/use-toast'; -import { useCart } from '@/hooks/useCart'; -import { AddressForm } from '@/components/checkout/AddressForm'; -import { PaymentMethodSelector } from '@/components/checkout/PaymentMethodSelector'; -import { CheckoutSummary } from '@/components/checkout/CheckoutSummary'; -import { OrderCreate, OrderItemCreate, PaymentMethod } from '@/types/order'; -import orderService from '@/lib/orders'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Separator } from '@/components/ui/separator'; -import { ArrowLeft, Loader2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { useSession } from 'next-auth/react'; -import Link from 'next/link'; - -interface AddressValues { - address_line1: string; - address_line2?: string; - city: string; - state: string; - postal_code: string; - country: string; - is_default: boolean; -} - -export default function CheckoutPage() { - const { data: session, status } = useSession(); - const { cart, loading: cartLoading, error: cartError } = useCart(); - const [address, setAddress] = useState(null); - const [paymentMethod, setPaymentMethod] = useState('credit_card'); - const [activeTab, setActiveTab] = useState('address'); - const [isSubmitting, setIsSubmitting] = useState(false); - const router = useRouter(); - const { toast } = useToast(); - - // Перенаправляем неавторизованных пользователей на страницу входа - useEffect(() => { - if (status === 'unauthenticated') { - router.push('/login?redirect=/checkout'); - } - }, [status, router]); - - // Перенаправляем пользователей с пустой корзиной обратно в корзину - useEffect(() => { - if (!cartLoading && cart && cart.items.length === 0) { - toast({ - title: 'Корзина пуста', - description: 'Перед оформлением заказа добавьте товары в корзину', - variant: 'destructive', - }); - router.push('/cart'); - } - }, [cart, cartLoading, router, toast]); - - if (status === 'loading' || cartLoading) { - return ( -
- -
- ); - } - - if (status === 'unauthenticated') { - return null; // Перенаправление происходит в useEffect - } - - if (cartError) { - return ( -
-
-

Произошла ошибка

-

{cartError}

- - - -
-
- ); - } - - const handleAddressSubmit = (values: AddressValues) => { - setAddress(values); - setActiveTab('payment'); - }; - - const handlePlaceOrder = async () => { - if (!address || !cart) return; - - try { - setIsSubmitting(true); - - // Этот метод для демонстрации. В реальном приложении здесь должно быть создание адреса - // и получение его ID для использования в заказе - const shippingAddressId = 1; // Placeholder ID для демонстрации - - // Создаем массив элементов заказа из товаров в корзине - const orderItems: OrderItemCreate[] = cart.items.map(item => ({ - variant_id: item.variant_id, - quantity: item.quantity - })); - - const orderData: OrderCreate = { - shipping_address_id: shippingAddressId, - payment_method: paymentMethod, - order_items: orderItems - }; - - const response = await orderService.createOrder(orderData); - - toast({ - title: 'Заказ оформлен успешно', - description: `Номер заказа: ${response.order?.id || 'не присвоен'}`, - }); - - // Предполагается, что заказ был создан успешно - router.push(`/orders/${response.order?.id || ''}`); - } catch (error) { - console.error('Ошибка при создании заказа:', error); - toast({ - title: 'Ошибка при оформлении заказа', - description: 'Пожалуйста, попробуйте еще раз или обратитесь в службу поддержки', - variant: 'destructive', - }); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
- -

Оформление заказа

-
- -
-
- - - Адрес доставки - Способ оплаты - - - - - Адрес доставки - - - - - - - - - - Способ оплаты - - - - - - - - - -
- -
- {cart && ( - - )} -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/components/.DS_Store b/frontend/components/.DS_Store index fbab188..c63e3e0 100644 Binary files a/frontend/components/.DS_Store and b/frontend/components/.DS_Store differ diff --git a/frontend/components/product/ProductDetails.tsx b/frontend/components/product/ProductDetails.tsx index a7bfdf7..338afba 100644 --- a/frontend/components/product/ProductDetails.tsx +++ b/frontend/components/product/ProductDetails.tsx @@ -56,7 +56,7 @@ export function ProductDetails({ product }: ProductDetailsProps) { .map(variant => ({ id: variant.id, size_id: variant.size_id, - size_name: variant.size?.name || variant.size?.code || '', + size_name: variant.size?.name || variant.size?.value || '', stock: variant.stock })) .filter((size, index, self) => @@ -116,7 +116,7 @@ export function ProductDetails({ product }: ProductDetailsProps) { product_id: safeProduct.id, variant_id: selectedSize || 0, quantity: quantity - }) + } as any) .then(() => { setAddedToCart(true); toast.success("Товар добавлен в корзину") @@ -138,7 +138,7 @@ export function ProductDetails({ product }: ProductDetailsProps) { product_id: safeProduct.id, variant_id: selectedSize || 0, quantity: quantity - }) + } as any) .then(() => { // Перенаправление на страницу корзины window.location.href = '/cart'; @@ -155,10 +155,11 @@ export function ProductDetails({ product }: ProductDetailsProps) { {safeProduct.variants && safeProduct.variants.length > 0 && (
-

РАЗМЕР

+

РАЗМЕР

@@ -175,13 +176,13 @@ export function ProductDetails({ product }: ProductDetailsProps) { ) : ( - 0 && !selectedSize} - /> + )} @@ -281,7 +285,7 @@ export function ProductDetails({ product }: ProductDetailsProps) {