feat: добавлена современная страница деталей услуги с портфолио и отзывами

This commit is contained in:
2025-11-24 09:05:48 +09:00
parent faa02b79c0
commit ee3a1bf846
20 changed files with 1260 additions and 53 deletions

View File

@@ -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"]
# 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"]

326
SERVICE_DETAIL_UPDATE.md Normal file
View File

@@ -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-кодом
Все изменения применены, статика собрана, контейнер перезапущен! 🚀

View File

@@ -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

11
docker-entrypoint-bot.sh Executable file
View File

@@ -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

17
docker-entrypoint.sh Executable file
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
]

View File

@@ -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;
}

View File

@@ -523,4 +523,369 @@ p {
right: 8px;
top: 38px;
}
}
}
/* 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;
}
}

View File

@@ -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);
})();

View File

@@ -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();
}
});
});

View File

@@ -7,13 +7,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="manifest" href="/static/manifest.json">
<script src="{% static 'assets/js/modal-init.js' %}"></script>
<link rel="stylesheet" href="{% static 'assets/css/modal-styles.css' %}">
<script src="{% static 'assets/js/modal-init.js' %}"></script>
<title>{% block title %}Smartsoltech{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'assets/css/styles.min.css' %}">
<!-- Force unblock script - загружается ПЕРВЫМ -->
<script src="{% static 'assets/js/force-unblock.js' %}"></script>
</head>
<body>
{% include 'web/navbar.html' %}
@@ -22,5 +21,6 @@
</div>
{% include 'web/footer.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{% static 'assets/js/modal-init.js' %}"></script>
</body>
</html>

View File

@@ -33,11 +33,14 @@
<title>{% block title %}SmartSolTech - Современные IT-решения{% endblock %}</title>
<!-- Force unblock script - загружается ПЕРВЫМ для предотвращения блокировки -->
<script src="{% static 'assets/js/force-unblock.js' %}"></script>
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen" class="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="background: var(--bg-light); z-index: 9999;">
<!-- Loading Screen - изначально НЕ блокирует клики -->
<div id="loading-screen" class="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="background: var(--bg-light); z-index: 9999; pointer-events: none;">
<div class="loading-spinner"></div>
</div>

View File

@@ -0,0 +1,268 @@
{% extends 'web/base_modern.html' %}
{% load static %}
{% block title %}{{ service.name }} - SmartSolTech{% endblock %}
{% block content %}
<!-- Hero Section with Service Info -->
<section class="service-hero py-5">
<div class="container">
<div class="row align-items-center g-4">
<div class="col-lg-6" data-aos="fade-right">
<div class="service-image-wrapper">
{% if service.image %}
<img src="{{ service.image.url }}" alt="{{ service.name }}" class="img-fluid rounded-4 shadow-lg">
{% else %}
<img src="{% static 'assets/img/placeholder-service.jpg' %}" alt="{{ service.name }}" class="img-fluid rounded-4 shadow-lg">
{% endif %}
</div>
</div>
<div class="col-lg-6" data-aos="fade-left">
<div class="service-info">
<div class="badge-category mb-3">
<i class="fas fa-tag me-2"></i>
{{ service.category.name|default:"Услуги" }}
</div>
<h1 class="service-title mb-4">{{ service.name }}</h1>
<!-- Rating and Reviews -->
<div class="service-stats d-flex align-items-center gap-4 mb-4">
<div class="rating-display">
<div class="stars">
{% for i in "12345" %}
{% if forloop.counter <= average_rating %}
<i class="fas fa-star text-warning"></i>
{% else %}
<i class="far fa-star text-warning"></i>
{% endif %}
{% endfor %}
</div>
<span class="ms-2 text-muted">{{ average_rating|floatformat:1 }} ({{ total_reviews }} отзывов)</span>
</div>
<div class="price-badge">
<span class="price-label">от</span>
<span class="price-value">{{ service.price|floatformat:0 }}</span>
<span class="price-currency"></span>
</div>
</div>
<p class="service-short-description lead mb-4">{{ service.description }}</p>
<button id="openModalBtn" class="btn btn-primary btn-lg" data-service-id="{{ service.id }}">
<i class="fas fa-paper-plane me-2"></i>
Оставить заявку
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Detailed Description Section -->
<section class="service-details py-5 bg-light">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10" data-aos="fade-up">
<div class="details-card card border-0 shadow-sm">
<div class="card-body p-4 p-lg-5">
<h2 class="section-title mb-4">
<i class="fas fa-info-circle me-3"></i>
Подробное описание
</h2>
<div class="detailed-description">
{{ service.description|linebreaks }}
</div>
<!-- Additional Info -->
<div class="service-features mt-5">
<div class="row g-4">
<div class="col-md-4">
<div class="feature-box text-center">
<div class="feature-icon mb-3">
<i class="fas fa-clock fa-2x text-primary"></i>
</div>
<h5>Быстрое выполнение</h5>
<p class="text-muted small">Оперативные сроки реализации проекта</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-box text-center">
<div class="feature-icon mb-3">
<i class="fas fa-award fa-2x text-primary"></i>
</div>
<h5>Качество</h5>
<p class="text-muted small">Гарантия высокого качества работ</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-box text-center">
<div class="feature-icon mb-3">
<i class="fas fa-headset fa-2x text-primary"></i>
</div>
<h5>Поддержка 24/7</h5>
<p class="text-muted small">Всегда на связи для решения ваших задач</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Portfolio Section -->
{% if service.projects.exists %}
<section class="portfolio-section py-5">
<div class="container">
<div class="text-center mb-5" data-aos="fade-up">
<h2 class="section-title">
<i class="fas fa-briefcase me-3"></i>
Портфолио проектов
</h2>
<p class="section-subtitle text-muted">Наши работы по данной услуге</p>
</div>
<div class="row g-4">
{% for project in service.projects.all %}
<div class="col-lg-4 col-md-6" data-aos="fade-up">
<div class="portfolio-card card border-0 shadow-sm h-100">
{% if project.image %}
<img src="{{ project.image.url }}" alt="{{ project.name }}" class="card-img-top portfolio-image">
{% else %}
<div class="portfolio-placeholder">
<i class="fas fa-project-diagram fa-3x text-muted"></i>
</div>
{% endif %}
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">{{ project.name }}</h5>
{% if project.status == 'completed' %}
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>Завершен
</span>
{% else %}
<span class="badge bg-info">
<i class="fas fa-spinner me-1"></i>В процессе
</span>
{% endif %}
</div>
<p class="card-text text-muted">{{ project.description|truncatewords:20 }}</p>
{% if project.completion_date %}
<div class="project-meta">
<small class="text-muted">
<i class="far fa-calendar me-2"></i>
{{ project.completion_date|date:"d.m.Y" }}
</small>
</div>
{% endif %}
<a href="{% url 'project_detail' project.pk %}" class="btn btn-outline-primary btn-sm mt-3 w-100">
<i class="fas fa-external-link-alt me-2"></i>
Подробнее о проекте
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Reviews Section -->
{% if reviews %}
<section class="reviews-section py-5 bg-light">
<div class="container">
<div class="text-center mb-5" data-aos="fade-up">
<h2 class="section-title">
<i class="fas fa-comments me-3"></i>
Отзывы клиентов
</h2>
<p class="section-subtitle text-muted">Что говорят о нас наши клиенты</p>
</div>
<div class="row g-4">
{% for review in reviews %}
<div class="col-lg-4 col-md-6" data-aos="fade-up">
<div class="review-card card border-0 shadow-sm h-100">
<div class="card-body">
<!-- Rating Stars -->
<div class="review-rating mb-3">
{% for i in "12345" %}
{% if forloop.counter <= review.rating %}
<i class="fas fa-star text-warning"></i>
{% else %}
<i class="far fa-star text-warning"></i>
{% endif %}
{% endfor %}
</div>
<p class="review-text mb-4">{{ review.comment }}</p>
<!-- Client Info -->
<div class="d-flex align-items-center">
{% if review.client.image %}
<img src="{{ review.client.image.url }}"
alt="{{ review.client.first_name }}"
class="rounded-circle me-3"
width="50"
height="50"
style="object-fit: cover;">
{% else %}
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; font-size: 1.2rem;">
{{ review.client.first_name|first }}{{ review.client.last_name|first }}
</div>
{% endif %}
<div>
<h6 class="mb-0">{{ review.client.first_name }} {{ review.client.last_name }}</h6>
<small class="text-muted">{{ review.review_date|date:"d.m.Y" }}</small>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- CTA Section -->
<section class="cta-section py-5">
<div class="container">
<div class="cta-card card border-0 shadow-lg" data-aos="zoom-in">
<div class="card-body p-5 text-center">
<h3 class="mb-4">Готовы начать свой проект?</h3>
<p class="lead text-muted mb-4">Оставьте заявку, и мы свяжемся с вами в ближайшее время</p>
<button id="openModalBtnCTA" class="btn btn-primary btn-lg px-5" data-service-id="{{ service.id }}">
<i class="fas fa-rocket me-2"></i>
Начать проект
</button>
</div>
</div>
</div>
</section>
<!-- Modal for Service Request -->
{% include "web/modal_order_form.html" %}
<!-- Scripts -->
<script src="{% static 'assets/js/get-csrf-token.js' %}"></script>
<script src="{% static 'assets/js/modal-init.js' %}"></script>
<script src="{% static 'assets/js/service_request.js' %}"></script>
<script src="{% static 'assets/js/verification_status.js' %}"></script>
<script>
// Обработчик для второй кнопки CTA
document.getElementById('openModalBtnCTA').addEventListener('click', function() {
document.getElementById('openModalBtn').click();
});
</script>
{% endblock %}

View File

@@ -26,23 +26,27 @@
<section class="section-padding">
<div class="container-modern">
<!-- Service Categories Filter -->
<div class="text-center mb-5">
<div class="btn-group" role="group" aria-label="Категории услуг">
<button type="button" class="btn btn-outline-primary active" data-filter="all">
Все услуги
</button>
<button type="button" class="btn btn-outline-primary" data-filter="web">
Веб-разработка
</button>
<button type="button" class="btn btn-outline-primary" data-filter="mobile">
Мобильные приложения
</button>
<button type="button" class="btn btn-outline-primary" data-filter="design">
Дизайн
</button>
<button type="button" class="btn btn-outline-primary" data-filter="other">
Другое
</button>
<div class="category-filters">
<a href="{% url 'services' %}" class="category-filter-btn {% if not selected_category %}active{% endif %}">
<i class="fas fa-th"></i>
<span>Все услуги</span>
</a>
{% for category in categories %}
<a href="{% url 'services' %}?category={{ category.id }}"
class="category-filter-btn {% if selected_category.id == category.id %}active{% endif %}">
<span>{{ category.name }}</span>
</a>
{% endfor %}
</div>
<!-- Services Count -->
<div class="text-center mb-4">
<div class="services-count">
{% if selected_category %}
Показано услуг в категории "<strong>{{ selected_category.name }}</strong>": <strong>{{ services.count }}</strong>
{% else %}
Всего услуг: <strong>{{ services.count }}</strong>
{% endif %}
</div>
</div>
@@ -101,6 +105,25 @@
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<div class="mb-4">
<i class="fas fa-inbox text-muted" style="font-size: 4rem;"></i>
</div>
<h4 class="text-muted mb-3">Услуги не найдены</h4>
<p class="text-muted mb-4">
{% if selected_category %}
В категории "{{ selected_category.name }}" пока нет доступных услуг.
{% else %}
На данный момент услуги не добавлены.
{% endif %}
</p>
<a href="{% url 'services' %}" class="btn btn-primary">
<i class="fas fa-th me-2"></i>Показать все услуги
</a>
</div>
</div>
{% endfor %}
</div>
</div>
@@ -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');

View File

@@ -0,0 +1 @@
# web/templatetags/__init__.py

View File

@@ -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

View File

@@ -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,

0
wait-for-it.sh Normal file → Executable file
View File