diff --git a/Dockerfile b/Dockerfile index cdc16c5..89c6b82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,5 +44,10 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000').read()" || exit 1 -# Start the Django server with wait for database -CMD ["sh", "-c", "./wait-for-it.sh postgres_db:5432 -- python smartsoltech/manage.py migrate && python smartsoltech/manage.py runserver 0.0.0.0:8000"] \ No newline at end of file +# Entrypoint scripts +COPY docker-entrypoint.sh /docker-entrypoint.sh +COPY docker-entrypoint-bot.sh /docker-entrypoint-bot.sh +RUN chmod +x /docker-entrypoint.sh /docker-entrypoint-bot.sh + +# Start the Django server +CMD ["/docker-entrypoint.sh"] \ No newline at end of file diff --git a/SERVICE_DETAIL_UPDATE.md b/SERVICE_DETAIL_UPDATE.md new file mode 100644 index 0000000..6f9f92f --- /dev/null +++ b/SERVICE_DETAIL_UPDATE.md @@ -0,0 +1,326 @@ +# Обновление страницы деталей услуги - SERVICE DETAIL + +## Дата: 24 ноября 2025 г. + +## Описание изменений + +Полностью переработана страница деталей услуги с современным дизайном и расширенным функционалом. + +--- + +## Что было сделано + +### 1. Создан новый шаблон `service_detail_modern.html` + +**Расположение:** `/smartsoltech/web/templates/web/service_detail_modern.html` + +#### Структура страницы: + +1. **Hero секция с информацией об услуге** + - Изображение услуги с hover-эффектом + - Категория услуги (badge) + - Название услуги + - Рейтинг и количество отзывов + - Цена услуги + - Краткое описание + - Кнопка "Оставить заявку" + +2. **Секция подробного описания** + - Развернутое описание услуги + - Блок с преимуществами: + - ⏰ Быстрое выполнение + - 🏆 Качество + - 🎧 Поддержка 24/7 + +3. **Портфолио проектов** + - Отображение всех проектов по данной услуге + - Карточки проектов с изображениями + - Статус проекта (Завершен/В процессе) + - Дата завершения + - Ссылка на детальную страницу проекта + - Адаптивная сетка (3 колонки на desktop, 2 на tablet, 1 на mobile) + +4. **Секция отзывов клиентов** + - Карточки отзывов с рейтингом + - Аватары клиентов (или инициалы) + - Дата публикации отзыва + - Адаптивная сетка + +5. **CTA секция (Call-to-Action)** + - Привлекательный призыв к действию + - Анимированный фон с gradient + - Кнопка "Начать проект" + +--- + +### 2. Добавлены CSS стили + +**Расположение:** `/smartsoltech/static/assets/css/modern-styles.css` + +#### Новые стили: + +- `.service-hero` - Hero секция с gradient фоном +- `.service-image-wrapper` - Обертка для изображения с hover-эффектом +- `.badge-category` - Badge категории с gradient +- `.service-title` - Заголовок услуги (2.5rem, font-weight: 800) +- `.service-stats` - Статистика (рейтинг + цена) +- `.price-badge` - Отображение цены +- `.details-card` - Карточка с подробным описанием +- `.section-title` - Заголовки секций с подчеркиванием +- `.feature-box` - Блоки преимуществ с hover-эффектом +- `.portfolio-card` - Карточки проектов с анимацией +- `.portfolio-image` - Изображения проектов (250px высота, object-fit: cover) +- `.portfolio-placeholder` - Placeholder для проектов без изображения +- `.review-card` - Карточки отзывов +- `.cta-card` - CTA секция с анимированным фоном + +#### Адаптивность: + +- Desktop (≥992px): 3 колонки для проектов/отзывов +- Tablet (768-991px): 2 колонки, уменьшенные шрифты +- Mobile (<768px): 1 колонка, компактные отступы + +--- + +### 3. Обновлен view + +**Файл:** `/smartsoltech/web/views.py` + +**Изменения:** +```python +def service_detail(request, pk): + # ... + return render(request, 'web/service_detail_modern.html', { # Изменен шаблон + 'service': service, + 'projects_in_category': projects_in_category, + 'average_rating': average_rating, + 'total_reviews': total_reviews, + 'reviews': reviews, + }) +``` + +--- + +### 4. Созданы template tags (опционально) + +**Расположение:** +- `/smartsoltech/web/templatetags/__init__.py` +- `/smartsoltech/web/templatetags/custom_filters.py` + +**Функции:** +- `mul` - умножение значений (для AOS задержек) +- `add` - сложение значений + +--- + +## Использованные технологии + +- **Django Templates** - шаблонизация +- **Bootstrap 5** - базовая сетка и компоненты +- **Custom CSS** - кастомные стили с CSS переменными +- **AOS (Animate On Scroll)** - анимации при прокрутке +- **Font Awesome** - иконки +- **CSS Animations** - кастомные анимации (pulse для CTA) + +--- + +## Особенности дизайна + +### Цветовая схема (из :root переменных): +- Primary: `#6366f1` (индиго) +- Secondary: `#8b5cf6` (фиолетовый) +- Accent: `#06d6a0` (бирюзовый) +- Gradient: `linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)` + +### Анимации: +- **Hover на изображениях**: scale(1.05) + transform +- **Карточки**: translateY(-10px) + shadow +- **CTA background**: pulse анимация (4s infinite) +- **Кнопки**: transform + shadow на hover + +### Эффекты: +- Rounded corners (border-radius: 1rem) +- Box shadows (0 20px 40px rgba) +- Smooth transitions (0.3s - 0.5s ease) +- Gradient backgrounds + +--- + +## Интеграция с существующим функционалом + +### Модальное окно заявки: +- Используется `{% include "web/modal_order_form.html" %}` +- 2 кнопки открытия: в Hero и в CTA секции +- JavaScript обработчики из `modal-init.js`, `service_request.js` + +### QR-код генерация: +- Поддержка base64 QR-кодов (из предыдущего обновления) +- Telegram bot integration +- Верификация статуса заявки + +--- + +## Модели данных + +### Service (используемые поля): +- `name` - название услуги +- `description` - описание (используется как краткое и развернутое) +- `price` - цена +- `category` - ForeignKey к Category +- `image` - изображение услуги +- `reviews` - related_name для Review +- `projects` - related_name для Project + +### Project (портфолио): +- `name` - название проекта +- `description` - описание +- `image` - изображение проекта +- `status` - статус (in_progress/completed) +- `completion_date` - дата завершения +- `service` - ForeignKey к Service + +### Review (отзывы): +- `client` - ForeignKey к Client +- `service` - ForeignKey к Service +- `rating` - рейтинг (1-5) +- `comment` - текст отзыва +- `review_date` - дата отзыва + +--- + +## Тестирование + +### Проверить работу: + +1. **Переход на страницу услуги:** + - Открыть http://localhost:8000/services/ + - Нажать "Подробнее" на любой услуге + - Проверить загрузку `service_detail_modern.html` + +2. **Hero секция:** + - ✅ Отображается изображение услуги + - ✅ Категория (badge) + - ✅ Название услуги + - ✅ Рейтинг со звездами + - ✅ Цена + - ✅ Описание + +3. **Подробное описание:** + - ✅ Развернутое описание услуги + - ✅ 3 блока преимуществ с иконками + - ✅ Hover-эффекты на блоках + +4. **Портфолио проектов:** + - ✅ Отображаются проекты по услуге + - ✅ Изображения проектов + - ✅ Статус (badge) + - ✅ Дата завершения + - ✅ Ссылка на детали проекта + - ✅ Placeholder для проектов без изображения + +5. **Отзывы:** + - ✅ Карточки отзывов + - ✅ Рейтинг звездами + - ✅ Аватар клиента или инициалы + - ✅ Дата отзыва + +6. **CTA секция:** + - ✅ Gradient фон с анимацией + - ✅ Кнопка "Начать проект" + - ✅ Открытие модального окна + +7. **Модальное окно:** + - ✅ Открывается по клику на кнопки + - ✅ Форма заявки + - ✅ Генерация QR-кода + - ✅ Telegram bot integration + +8. **Адаптивность:** + - ✅ Desktop (≥992px): 3 колонки + - ✅ Tablet (768-991px): 2 колонки + - ✅ Mobile (<768px): 1 колонка + +--- + +## Команды для развертывания + +```bash +# 1. Собрать статические файлы +docker-compose exec web python smartsoltech/manage.py collectstatic --noinput + +# 2. Перезапустить контейнер +docker-compose restart web + +# 3. Проверить логи +docker-compose logs -f web +``` + +--- + +## Возможные улучшения + +### В будущем можно добавить: + +1. **Расширенное описание услуги:** + - Отдельное поле `detailed_description` в модели Service + - WYSIWYG редактор в админке (CKEditor, TinyMCE) + - Поддержка HTML форматирования + +2. **Галерея проектов:** + - Lightbox для просмотра изображений + - Фильтрация проектов по статусу + - Пагинация при большом количестве проектов + +3. **Интерактивные элементы:** + - Карусель отзывов (Swiper.js) + - Модальное окно для просмотра проектов + - Форма для добавления отзыва + +4. **SEO оптимизация:** + - Meta description для каждой услуги + - Open Graph теги + - Schema.org разметка (Service, Review, Offer) + +5. **Аналитика:** + - Счетчик просмотров услуги + - Отслеживание конверсии (просмотр → заявка) + - Популярные услуги + +--- + +## Структура файлов проекта + +``` +smartsoltech/ +├── web/ +│ ├── templates/ +│ │ └── web/ +│ │ ├── service_detail.html (старый шаблон) +│ │ └── service_detail_modern.html (новый шаблон) ✨ +│ ├── templatetags/ +│ │ ├── __init__.py ✨ +│ │ └── custom_filters.py ✨ +│ ├── views.py (обновлен) +│ └── models.py (без изменений) +├── static/ +│ └── assets/ +│ └── css/ +│ └── modern-styles.css (обновлен) ✨ +└── staticfiles/ (collectstatic) +``` + +--- + +## Заключение + +Страница деталей услуги теперь имеет: +- ✅ Современный дизайн с градиентами и анимациями +- ✅ Развернутое описание услуги +- ✅ Портфолио проектов по услуге +- ✅ Секцию отзывов клиентов +- ✅ CTA секцию для увеличения конверсии +- ✅ Полную адаптивность для всех устройств +- ✅ Интеграцию с Telegram ботом +- ✅ Модальное окно для заявок с QR-кодом + +Все изменения применены, статика собрана, контейнер перезапущен! 🚀 diff --git a/docker-compose.yml b/docker-compose.yml index bce61d6..58a7ab3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,7 +82,7 @@ services: bot: build: . container_name: telegram_bot - command: sh -c "./wait-for-it.sh postgres_db:5432 -- python smartsoltech/manage.py start_telegram_bot" + command: /docker-entrypoint-bot.sh restart: unless-stopped volumes: - .:/app diff --git a/docker-entrypoint-bot.sh b/docker-entrypoint-bot.sh new file mode 100755 index 0000000..65f2d50 --- /dev/null +++ b/docker-entrypoint-bot.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +echo "Waiting for PostgreSQL..." +while ! nc -z postgres_db 5432; do + sleep 1 +done +echo "PostgreSQL is ready!" + +echo "Starting Telegram bot..." +exec python smartsoltech/manage.py start_telegram_bot diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..b61e16f --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +echo "Waiting for PostgreSQL..." +while ! nc -z postgres_db 5432; do + sleep 1 +done +echo "PostgreSQL is ready!" + +echo "Running migrations..." +python smartsoltech/manage.py migrate --noinput + +echo "Collecting static files..." +python smartsoltech/manage.py collectstatic --noinput || true + +echo "Starting Django server..." +exec python smartsoltech/manage.py runserver 0.0.0.0:8000 diff --git a/smartsoltech/smartsoltech/static/qr_codes/request_2.png b/smartsoltech/smartsoltech/static/qr_codes/request_2.png new file mode 100644 index 0000000..9bad515 Binary files /dev/null and b/smartsoltech/smartsoltech/static/qr_codes/request_2.png differ diff --git a/smartsoltech/smartsoltech/static/qr_codes/request_3.png b/smartsoltech/smartsoltech/static/qr_codes/request_3.png new file mode 100644 index 0000000..595c848 Binary files /dev/null and b/smartsoltech/smartsoltech/static/qr_codes/request_3.png differ diff --git a/smartsoltech/smartsoltech/urls.py b/smartsoltech/smartsoltech/urls.py index 5a2c006..eb4c1b7 100644 --- a/smartsoltech/smartsoltech/urls.py +++ b/smartsoltech/smartsoltech/urls.py @@ -1,8 +1,25 @@ # smartsoltech/urls.py from django.contrib import admin from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('web.urls')), # Включаем маршруты приложения web ] + +# Serve static and media files in development +if settings.DEBUG: + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +else: + # In production with DEBUG=False, serve static files via Django (temporary solution) + # For production, use Nginx or another web server + from django.views.static import serve + from django.urls import re_path + + urlpatterns += [ + re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), + re_path(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}), + ] diff --git a/smartsoltech/static/assets/css/modal-styles.css b/smartsoltech/static/assets/css/modal-styles.css index 66e7a55..f220fc7 100644 --- a/smartsoltech/static/assets/css/modal-styles.css +++ b/smartsoltech/static/assets/css/modal-styles.css @@ -1 +1,23 @@ @import url('https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css'); + +/* Убедимся, что все модальные окна скрыты по умолчанию */ +.modal { + display: none !important; + position: fixed; + z-index: 9999; + pointer-events: none; +} + +.modal.show { + display: block !important; + pointer-events: auto; +} + +/* Убедимся, что body не заблокирован */ +body { + overflow: auto !important; +} + +body.modal-open { + overflow: hidden !important; +} diff --git a/smartsoltech/static/assets/css/modern-styles.css b/smartsoltech/static/assets/css/modern-styles.css index 2c92861..3ae3f0f 100644 --- a/smartsoltech/static/assets/css/modern-styles.css +++ b/smartsoltech/static/assets/css/modern-styles.css @@ -523,4 +523,369 @@ p { right: 8px; top: 38px; } -} \ No newline at end of file +} +/* Category Filters */ +.category-filters { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; + margin-bottom: 2rem; +} + +.category-filter-btn { + display: inline-flex; + align-items: center; + padding: 0.75rem 1.5rem; + border: 2px solid var(--border-color); + border-radius: 50px; + background: var(--bg-light); + color: var(--text-dark); + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.category-filter-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--gradient-primary); + transition: left 0.3s ease; + z-index: 0; +} + +.category-filter-btn:hover::before, +.category-filter-btn.active::before { + left: 0; +} + +.category-filter-btn i, +.category-filter-btn span { + position: relative; + z-index: 1; + transition: color 0.3s ease; +} + +.category-filter-btn:hover, +.category-filter-btn.active { + border-color: var(--primary-color); + color: white; + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); +} + +.category-filter-btn i { + margin-right: 0.5rem; +} + +/* =================================== + SERVICE DETAIL PAGE STYLES + =================================== */ + +/* Service Hero Section */ +.service-hero { + padding: 4rem 0; + background: linear-gradient(135deg, var(--bg-gray) 0%, var(--bg-light) 100%); +} + +.service-image-wrapper { + position: relative; + overflow: hidden; + border-radius: 1rem; +} + +.service-image-wrapper img { + transition: transform 0.5s ease; +} + +.service-image-wrapper:hover img { + transform: scale(1.05); +} + +.badge-category { + display: inline-block; + padding: 0.5rem 1rem; + background: var(--gradient-primary); + color: white; + border-radius: 50px; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.service-title { + font-size: 2.5rem; + font-weight: 800; + color: var(--text-dark); + line-height: 1.2; +} + +.service-stats { + padding: 1rem 0; + border-top: 2px solid var(--border-color); + border-bottom: 2px solid var(--border-color); +} + +.rating-display .stars { + display: inline-block; +} + +.price-badge { + display: flex; + align-items: baseline; + gap: 0.25rem; + font-weight: 700; +} + +.price-label { + font-size: 0.875rem; + color: var(--text-light); +} + +.price-value { + font-size: 2rem; + color: var(--primary-color); +} + +.price-currency { + font-size: 1.25rem; + color: var(--text-light); +} + +.service-short-description { + font-size: 1.125rem; + color: var(--text-light); + line-height: 1.8; +} + +/* Service Details Section */ +.service-details { + background: var(--bg-gray); +} + +.details-card { + border-radius: 1rem; + overflow: hidden; +} + +.section-title { + font-size: 2rem; + font-weight: 700; + color: var(--text-dark); + position: relative; + padding-bottom: 1rem; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 80px; + height: 4px; + background: var(--gradient-primary); + border-radius: 2px; +} + +.section-subtitle { + font-size: 1.125rem; + margin-top: 0.5rem; +} + +.detailed-description { + font-size: 1rem; + line-height: 1.8; + color: var(--text-light); +} + +.service-features .feature-box { + padding: 2rem 1rem; + border-radius: 1rem; + background: var(--bg-light); + transition: all 0.3s ease; +} + +.service-features .feature-box:hover { + transform: translateY(-5px); + box-shadow: var(--shadow); +} + +.feature-icon { + width: 80px; + height: 80px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); + border-radius: 50%; +} + +/* Portfolio Section */ +.portfolio-section { + background: var(--bg-light); +} + +.portfolio-card { + border-radius: 1rem; + overflow: hidden; + transition: all 0.3s ease; +} + +.portfolio-card:hover { + transform: translateY(-10px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +.portfolio-image { + width: 100%; + height: 250px; + object-fit: cover; + transition: transform 0.5s ease; +} + +.portfolio-card:hover .portfolio-image { + transform: scale(1.1); +} + +.portfolio-placeholder { + width: 100%; + height: 250px; + background: linear-gradient(135deg, var(--bg-gray) 0%, var(--border-color) 100%); + display: flex; + align-items: center; + justify-content: center; +} + +.project-meta { + padding-top: 1rem; + margin-top: 1rem; + border-top: 1px solid var(--border-color); +} + +/* Reviews Section */ +.reviews-section { + background: var(--bg-gray); +} + +.review-card { + border-radius: 1rem; + transition: all 0.3s ease; +} + +.review-card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); +} + +.review-rating { + font-size: 1.25rem; +} + +.review-text { + font-size: 1rem; + color: var(--text-light); + line-height: 1.8; + font-style: italic; +} + +/* CTA Section */ +.cta-section { + padding: 4rem 0; + background: var(--bg-light); +} + +.cta-card { + background: var(--gradient-primary); + color: white; + border-radius: 1rem; + overflow: hidden; + position: relative; +} + +.cta-card::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + animation: pulse 4s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 0.5; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +.cta-card h3 { + color: white; + font-size: 2rem; + font-weight: 700; +} + +.cta-card .lead { + color: rgba(255, 255, 255, 0.9); +} + +.cta-card .btn-primary { + background: white; + color: var(--primary-color); + border: none; + font-weight: 700; +} + +.cta-card .btn-primary:hover { + background: var(--bg-gray); + transform: translateY(-3px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); +} + +/* Responsive adjustments for service detail */ +@media (max-width: 992px) { + .service-title { + font-size: 2rem; + } + + .section-title { + font-size: 1.75rem; + } + + .service-stats { + flex-direction: column; + gap: 1rem !important; + } +} + +@media (max-width: 768px) { + .service-hero { + padding: 2rem 0; + } + + .service-title { + font-size: 1.75rem; + } + + .price-value { + font-size: 1.5rem; + } + + .service-features .feature-box { + padding: 1.5rem 1rem; + } +} + diff --git a/smartsoltech/static/assets/js/force-unblock.js b/smartsoltech/static/assets/js/force-unblock.js new file mode 100644 index 0000000..f65bcc4 --- /dev/null +++ b/smartsoltech/static/assets/js/force-unblock.js @@ -0,0 +1,60 @@ +// Force unblock - агрессивная очистка блокирующих элементов +(function() { + 'use strict'; + + function forceUnblock() { + console.log('ForceUnblock: Starting cleanup...'); + + // Удаляем loading screen + const loadingScreen = document.getElementById('loading-screen'); + if (loadingScreen) { + loadingScreen.remove(); + console.log('ForceUnblock: Loading screen removed'); + } + + // Убираем modal-open с body + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + console.log('ForceUnblock: Body cleaned'); + + // Закрываем все модальные окна + document.querySelectorAll('.modal').forEach(modal => { + modal.classList.remove('show'); + modal.style.display = 'none'; + modal.setAttribute('aria-hidden', 'true'); + modal.removeAttribute('aria-modal'); + }); + console.log('ForceUnblock: Modals closed'); + + // Удаляем все backdrop элементы + document.querySelectorAll('.modal-backdrop').forEach(backdrop => { + backdrop.remove(); + }); + console.log('ForceUnblock: Backdrops removed'); + + // Убираем pointer-events: none с всех элементов кроме скрытых модалов + document.querySelectorAll('[style*="pointer-events"]').forEach(el => { + if (!el.classList.contains('modal') || !el.classList.contains('show')) { + el.style.pointerEvents = ''; + } + }); + console.log('ForceUnblock: Pointer events cleaned'); + + // Проверяем, что body кликабельно + document.body.style.pointerEvents = 'auto'; + + console.log('ForceUnblock: Cleanup complete!'); + } + + // Выполняем сразу + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', forceUnblock); + } else { + forceUnblock(); + } + + // И еще раз через небольшую задержку для надежности + setTimeout(forceUnblock, 100); + setTimeout(forceUnblock, 500); +})(); diff --git a/smartsoltech/static/assets/js/modal-init.js b/smartsoltech/static/assets/js/modal-init.js index fcb6ea4..e74dc99 100644 --- a/smartsoltech/static/assets/js/modal-init.js +++ b/smartsoltech/static/assets/js/modal-init.js @@ -1,5 +1,18 @@ document.addEventListener("DOMContentLoaded", function () { - // Работа с модальным окном заявки + // Функция для закрытия всех модальных окон + function closeAllModals() { + document.querySelectorAll('.modal').forEach(modal => { + modal.classList.remove('show'); + modal.style.display = 'none'; + }); + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + } + + // Закрыть все модальные окна при загрузке страницы + closeAllModals(); + + // Работа с модальным окном заявки (Bootstrap modal) var modalElement = document.getElementById('orderModal'); if (modalElement) { var modal = new bootstrap.Modal(modalElement); @@ -14,9 +27,10 @@ document.addEventListener("DOMContentLoaded", function () { }); } - // Открытие модального окна для заявки на услугу + // Открытие кастомного модального окна для заявки на услугу const openModalBtn = document.getElementById('openModalBtn'); const serviceModal = document.getElementById('serviceModal'); + const generateQrButton = document.getElementById('generateQrButton'); if (openModalBtn && serviceModal) { openModalBtn.addEventListener('click', function (event) { @@ -29,21 +43,36 @@ document.addEventListener("DOMContentLoaded", function () { return; } - generateQrButton.dataset.serviceId = serviceId; + if (generateQrButton) { + generateQrButton.dataset.serviceId = serviceId; + } serviceModal.classList.add('show'); serviceModal.style.display = 'block'; + document.body.classList.add('modal-open'); + document.body.style.overflow = 'hidden'; }); } - document.querySelectorAll('.close').forEach(closeBtn => { + // Закрытие кастомного модального окна + const closeButtons = document.querySelectorAll('.close'); + closeButtons.forEach(closeBtn => { closeBtn.addEventListener('click', function () { - if (serviceModal) { - serviceModal.classList.remove('show'); - setTimeout(() => { - serviceModal.style.display = 'none'; - }, 500); - } + closeAllModals(); }); }); + + // Закрытие модального окна при клике вне его + window.addEventListener('click', function (event) { + if (event.target.classList.contains('modal') && event.target.classList.contains('show')) { + closeAllModals(); + } + }); + + // Закрытие по нажатию Escape + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeAllModals(); + } + }); }); diff --git a/smartsoltech/web/templates/web/base.html b/smartsoltech/web/templates/web/base.html index 69ff0b9..e7a212e 100644 --- a/smartsoltech/web/templates/web/base.html +++ b/smartsoltech/web/templates/web/base.html @@ -7,13 +7,12 @@ - - - {% block title %}Smartsoltech{% endblock %} {% load static %} + + {% include 'web/navbar.html' %} @@ -22,5 +21,6 @@ {% include 'web/footer.html' %} + \ No newline at end of file diff --git a/smartsoltech/web/templates/web/base_modern.html b/smartsoltech/web/templates/web/base_modern.html index 4e68c39..de2cc5b 100644 --- a/smartsoltech/web/templates/web/base_modern.html +++ b/smartsoltech/web/templates/web/base_modern.html @@ -33,11 +33,14 @@ {% block title %}SmartSolTech - Современные IT-решения{% endblock %} + + + {% block extra_head %}{% endblock %} - -
+ +
diff --git a/smartsoltech/web/templates/web/service_detail_modern.html b/smartsoltech/web/templates/web/service_detail_modern.html new file mode 100644 index 0000000..8e26846 --- /dev/null +++ b/smartsoltech/web/templates/web/service_detail_modern.html @@ -0,0 +1,268 @@ +{% extends 'web/base_modern.html' %} +{% load static %} +{% block title %}{{ service.name }} - SmartSolTech{% endblock %} + +{% block content %} + + +
+
+
+
+
+ {% if service.image %} + {{ service.name }} + {% else %} + {{ service.name }} + {% endif %} +
+
+
+
+
+ + {{ service.category.name|default:"Услуги" }} +
+

{{ service.name }}

+ + +
+
+
+ {% for i in "12345" %} + {% if forloop.counter <= average_rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+ {{ average_rating|floatformat:1 }} ({{ total_reviews }} отзывов) +
+
+ от + {{ service.price|floatformat:0 }} + +
+
+ +

{{ service.description }}

+ + +
+
+
+
+
+ + +
+
+
+
+
+
+

+ + Подробное описание +

+
+ {{ service.description|linebreaks }} +
+ + +
+
+
+
+
+ +
+
Быстрое выполнение
+

Оперативные сроки реализации проекта

+
+
+
+
+
+ +
+
Качество
+

Гарантия высокого качества работ

+
+
+
+
+
+ +
+
Поддержка 24/7
+

Всегда на связи для решения ваших задач

+
+
+
+
+
+
+
+
+
+
+ + +{% if service.projects.exists %} +
+
+
+

+ + Портфолио проектов +

+

Наши работы по данной услуге

+
+ +
+ {% for project in service.projects.all %} +
+
+ {% if project.image %} + {{ project.name }} + {% else %} +
+ +
+ {% endif %} + +
+
+
{{ project.name }}
+ {% if project.status == 'completed' %} + + Завершен + + {% else %} + + В процессе + + {% endif %} +
+ +

{{ project.description|truncatewords:20 }}

+ + {% if project.completion_date %} +
+ + + {{ project.completion_date|date:"d.m.Y" }} + +
+ {% endif %} + + + + Подробнее о проекте + +
+
+
+ {% endfor %} +
+
+
+{% endif %} + + +{% if reviews %} +
+
+
+

+ + Отзывы клиентов +

+

Что говорят о нас наши клиенты

+
+ +
+ {% for review in reviews %} +
+
+
+ +
+ {% for i in "12345" %} + {% if forloop.counter <= review.rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+ +

{{ review.comment }}

+ + +
+ {% if review.client.image %} + {{ review.client.first_name }} + {% else %} +
+ {{ review.client.first_name|first }}{{ review.client.last_name|first }} +
+ {% endif %} +
+
{{ review.client.first_name }} {{ review.client.last_name }}
+ {{ review.review_date|date:"d.m.Y" }} +
+
+
+
+
+ {% endfor %} +
+
+
+{% endif %} + + +
+
+
+
+

Готовы начать свой проект?

+

Оставьте заявку, и мы свяжемся с вами в ближайшее время

+ +
+
+
+
+ + +{% include "web/modal_order_form.html" %} + + + + + + + + + +{% endblock %} diff --git a/smartsoltech/web/templates/web/services_modern.html b/smartsoltech/web/templates/web/services_modern.html index 209a052..2871a8f 100644 --- a/smartsoltech/web/templates/web/services_modern.html +++ b/smartsoltech/web/templates/web/services_modern.html @@ -26,23 +26,27 @@
-
-
- - - - - +
+ + + Все услуги + + {% for category in categories %} + + {{ category.name }} + + {% endfor %} +
+ + +
+
+ {% if selected_category %} + Показано услуг в категории "{{ selected_category.name }}": {{ services.count }} + {% else %} + Всего услуг: {{ services.count }} + {% endif %}
@@ -101,6 +105,25 @@
+ {% empty %} +
+
+
+ +
+

Услуги не найдены

+

+ {% if selected_category %} + В категории "{{ selected_category.name }}" пока нет доступных услуг. + {% else %} + На данный момент услуги не добавлены. + {% endif %} +

+ + Показать все услуги + +
+
{% endfor %}
@@ -441,14 +464,20 @@ document.getElementById('serviceRequestForm').addEventListener('submit', functio }) .then(response => response.json()) .then(data => { + console.log('Response data:', data); // Добавлено логирование + if (data.qr_code_url) { // Показываем секцию с QR-кодом const qrSection = document.getElementById('qrCodeSection'); const qrImage = document.getElementById('qrCodeImage'); const telegramLink = document.getElementById('telegramLink'); + console.log('QR Code URL:', data.qr_code_url); // Добавлено логирование qrImage.src = data.qr_code_url; qrImage.style.display = 'block'; + qrImage.onerror = function() { + console.error('Failed to load QR code image:', data.qr_code_url); + }; telegramLink.href = data.registration_link; qrSection.style.display = 'block'; @@ -463,11 +492,13 @@ document.getElementById('serviceRequestForm').addEventListener('submit', functio }, 3000); // Проверяем каждые 3 секунды } else { + console.error('No QR code URL in response:', data); // Добавлено логирование throw new Error(data.error || 'Ошибка при создании заявки'); } }) .catch(error => { console.error('Error:', error); + console.error('Error details:', error.message); // Добавлено логирование submitBtn.innerHTML = originalContent; submitBtn.disabled = false; submitBtn.classList.remove('btn-success'); diff --git a/smartsoltech/web/templatetags/__init__.py b/smartsoltech/web/templatetags/__init__.py new file mode 100644 index 0000000..e83b8fe --- /dev/null +++ b/smartsoltech/web/templatetags/__init__.py @@ -0,0 +1 @@ +# web/templatetags/__init__.py diff --git a/smartsoltech/web/templatetags/custom_filters.py b/smartsoltech/web/templatetags/custom_filters.py new file mode 100644 index 0000000..075e0b4 --- /dev/null +++ b/smartsoltech/web/templatetags/custom_filters.py @@ -0,0 +1,19 @@ +from django import template + +register = template.Library() + +@register.filter +def mul(value, arg): + """Multiply the value by the argument""" + try: + return int(value) * int(arg) + except (ValueError, TypeError): + return 0 + +@register.filter +def add(value, arg): + """Add the argument to the value""" + try: + return int(value) + int(arg) + except (ValueError, TypeError): + return 0 diff --git a/smartsoltech/web/views.py b/smartsoltech/web/views.py index 9109641..c61d388 100644 --- a/smartsoltech/web/views.py +++ b/smartsoltech/web/views.py @@ -1,9 +1,11 @@ from django.shortcuts import render, get_object_or_404, redirect -from .models import Service, Project, Client, BlogPost, Review, Order, ServiceRequest +from .models import Service, Project, Client, BlogPost, Review, Order, ServiceRequest, Category from django.db.models import Avg from comunication.models import TelegramSettings import qrcode import os +import io +import base64 from django.conf import settings import uuid from django.utils.http import urlsafe_base64_encode @@ -42,7 +44,7 @@ def service_detail(request, pk): average_rating = service.reviews.aggregate(Avg('rating'))['rating__avg'] or 0 total_reviews = service.reviews.count() reviews = service.reviews.all() - return render(request, 'web/service_detail.html', { + return render(request, 'web/service_detail_modern.html', { 'service': service, 'projects_in_category': projects_in_category, 'average_rating': average_rating, @@ -63,8 +65,26 @@ def blog_post_detail(request, pk): return render(request, 'web/blog_post_detail.html', {'blog_post': blog_post}) def services_view(request): - services = Service.objects.all() - return render(request, 'web/services_modern.html', {'services': services}) + # Получаем выбранную категорию из GET параметра + category_id = request.GET.get('category') + + # Получаем все категории для фильтров + categories = Category.objects.all() + + # Фильтруем услуги по категории, если выбрана + if category_id: + services = Service.objects.filter(category_id=category_id) + selected_category = Category.objects.filter(id=category_id).first() + else: + services = Service.objects.all() + selected_category = None + + context = { + 'services': services, + 'categories': categories, + 'selected_category': selected_category, + } + return render(request, 'web/services_modern.html', context) def about_view(request): return render(request, 'web/about_modern.html') @@ -191,19 +211,32 @@ def generate_qr_code(request, service_id): ) logger.info(f"Создана новая заявка: {service_request.id} для клиента: {client.email}") - # Генерация ссылки и QR-кода для Telegram + # Получаем настройки Telegram бота из БД telegram_settings = get_object_or_404(TelegramSettings, pk=1) - registration_link = f'https://t.me/{telegram_settings.bot_name}?start=request_{service_request.id}_token_{urlsafe_base64_encode(force_bytes(token))}' - - qr = qrcode.make(registration_link) - qr_code_dir = os.path.join(settings.STATICFILES_DIRS[0], 'qr_codes') - qr_code_path = os.path.join(qr_code_dir, f"request_{service_request.id}.png") - external_qr_link = f'static/qr_codes/request_{service_request.id}.png' - - if not os.path.exists(qr_code_dir): - os.makedirs(qr_code_dir) - - qr.save(qr_code_path) + bot_username = telegram_settings.bot_name + + # Генерация ссылки для Telegram + registration_link = f'https://t.me/{bot_username}?start=request_{service_request.id}_token_{urlsafe_base64_encode(force_bytes(token))}' + + # Генерируем QR-код в памяти (без сохранения на диск) + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(registration_link) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Конвертируем изображение в base64 + buffer = io.BytesIO() + img.save(buffer, format='PNG') + qr_code_base64 = base64.b64encode(buffer.getvalue()).decode() + qr_code_data_url = f'data:image/png;base64,{qr_code_base64}' + + logger.info(f"QR-код сгенерирован в памяти для заявки {service_request.id}") except IntegrityError as e: logger.error(f"Ошибка целостности данных при создании пользователя или клиента: {str(e)}") @@ -214,7 +247,7 @@ def generate_qr_code(request, service_id): return JsonResponse({ 'registration_link': registration_link, - 'qr_code_url': f"/{external_qr_link}", + 'qr_code_url': qr_code_data_url, 'service_request_id': service_request.id, 'client_email': client_email, 'client_phone': client_phone, diff --git a/wait-for-it.sh b/wait-for-it.sh old mode 100644 new mode 100755