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

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