Compare commits

...

14 Commits

Author SHA1 Message Date
10846519e3 Исправлена валидация формы заказа услуги - поля теперь правильно передаются и проверяются
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-26 21:42:09 +09:00
991a9b104e Исправлена форма заказа услуги для показа QR-кода вместо создания заявки напрямую
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 21:30:44 +09:00
5349b3c37f Исправлена ошибка создания ServiceRequest - убран несуществующий параметр message и добавлено создание Order
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 21:24:11 +09:00
2479406d3d Исправлены HTML теги в сервисах и восстановлена кликабельность кнопок на странице деталей сервиса
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 21:14:24 +09:00
a0a20d7270 Убрана секция карьеры с главной страницы и обновлены категории портфолио на овальные пилюли
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 21:07:14 +09:00
f72a4d5a5b Удален раздел новостей с главной страницы и реализованы овальные пилюли для категорий проектов
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 21:02:33 +09:00
803c1373e0 Fix HTML tags display in project descriptions on homepage
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 20:55:27 +09:00
25d797dff0 asdasd
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 19:47:58 +09:00
986001814c recreate table migration
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 19:16:20 +09:00
ccc66f7f0d Add project slug field migration
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-26 19:06:10 +09:00
b51d79c5a1 Implement modern media gallery with enhanced features
Some checks failed
continuous-integration/drone/push Build is failing
- Fix CSS loading issue in project_detail.html template
- Add comprehensive ModernMediaGallery JavaScript class with touch navigation
- Implement glassmorphism design with backdrop-filter effects
- Add responsive breakpoint system for mobile devices
- Include embedded critical CSS styles for gallery functionality
- Add technology sidebar with vertical list layout and hover effects
- Support for images, videos, and embedded content with thumbnails
- Add lightbox integration and media type badges
- Implement progress bar and thumbnail navigation
- Add keyboard controls (arrow keys) and touch swipe gestures
- Include supplementary styles for video/embed placeholders
- Fix template block naming compatibility (extra_css → extra_styles)
2025-11-26 18:52:07 +09:00
8e1751ef5d Remove ckeditor_uploader dependency and replace with TextField
Some checks failed
continuous-integration/drone/push Build is failing
- Removed ckeditor_uploader==6.4.2 from requirements.txt
- Modified migration 0014 to use models.TextField instead of ckeditor fields
- Replaced RichTextUploadingField with standard TextField for blog content and portfolio descriptions
- This resolves dependency issues while maintaining data compatibility
2025-11-26 10:37:12 +09:00
a2a3b0a842 Merge branch 'master' of ssh://git.smartsoltech.kr:2222/trevor/smartsoltech_site
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-26 10:09:13 +09:00
a90e046e03 Add docker-compose compatibility script for production server
- Script creates symlink or wrapper for docker-compose command
- Automatically detects Docker Compose v2 plugin location
- Fallback to wrapper script if plugin not found
- Helps maintain compatibility with existing deployment scripts
2025-11-26 10:08:59 +09:00
26 changed files with 9739 additions and 578 deletions

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Скрипт для настройки совместимости docker-compose с Docker Compose v2
# Запускать на продакшн сервере с правами sudo
echo "🐳 Setting up docker-compose compatibility..."
# Проверяем, есть ли уже docker-compose
if command -v docker-compose >/dev/null 2>&1; then
echo "✅ docker-compose уже доступен:"
docker-compose --version
exit 0
fi
# Проверяем наличие docker compose v2
if ! docker compose version >/dev/null 2>&1; then
echo "❌ Docker Compose v2 не найден. Установите Docker сначала."
exit 1
fi
echo "📦 Docker Compose v2 обнаружен:"
docker compose version
# Пытаемся найти путь к docker-compose plugin
COMPOSE_PLUGIN_PATH=""
for path in "/usr/libexec/docker/cli-plugins/docker-compose" "/usr/local/lib/docker/cli-plugins/docker-compose" "/opt/docker/cli-plugins/docker-compose"; do
if [ -f "$path" ]; then
COMPOSE_PLUGIN_PATH="$path"
break
fi
done
# Если найден plugin, создаем symlink
if [ -n "$COMPOSE_PLUGIN_PATH" ]; then
echo "🔗 Создаем symlink из $COMPOSE_PLUGIN_PATH"
sudo ln -sf "$COMPOSE_PLUGIN_PATH" /usr/local/bin/docker-compose
else
# Создаем wrapper скрипт
echo "📝 Создаем wrapper скрипт..."
sudo tee /usr/local/bin/docker-compose > /dev/null << 'EOF'
#!/bin/bash
# Docker Compose v1 compatibility wrapper
exec docker compose "$@"
EOF
fi
# Делаем исполняемым
sudo chmod +x /usr/local/bin/docker-compose
# Проверяем результат
if command -v docker-compose >/dev/null 2>&1; then
echo "✅ Успешно! docker-compose теперь доступен:"
docker-compose --version
else
echo "❌ Что-то пошло не так. Проверьте настройки PATH."
exit 1
fi
echo "🎉 Настройка завершена!"

192
create_test_projects.py Normal file
View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
import os
import sys
import django
from datetime import datetime, date
# Настройка Django
sys.path.append('/app/smartsoltech')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartsoltech.settings')
django.setup()
from web.models import Project, Category, Client, Service, Order
from django.contrib.auth.models import User
def create_test_projects():
# Создаем или получаем категории
categories = []
cat_web, _ = Category.objects.get_or_create(
slug='web-development',
defaults={
'name': 'Веб-разработка',
'icon': 'fas fa-code',
'order': 1,
'description': 'Создание веб-сайтов и приложений'
}
)
categories.append(cat_web)
cat_mobile, _ = Category.objects.get_or_create(
slug='mobile-apps',
defaults={
'name': 'Мобильные приложения',
'icon': 'fas fa-mobile-alt',
'order': 2,
'description': 'Разработка iOS и Android приложений'
}
)
categories.append(cat_mobile)
cat_design, _ = Category.objects.get_or_create(
slug='design',
defaults={
'name': 'Дизайн',
'icon': 'fas fa-palette',
'order': 3,
'description': 'UI/UX дизайн и брендинг'
}
)
categories.append(cat_design)
cat_analytics, _ = Category.objects.get_or_create(
slug='analytics',
defaults={
'name': 'Аналитика',
'icon': 'fas fa-chart-bar',
'order': 4,
'description': 'Системы аналитики и отчетности'
}
)
categories.append(cat_analytics)
cat_ecommerce, _ = Category.objects.get_or_create(
slug='ecommerce',
defaults={
'name': 'E-commerce',
'icon': 'fas fa-shopping-cart',
'order': 5,
'description': 'Интернет-магазины и торговые платформы'
}
)
categories.append(cat_ecommerce)
# Создаем или получаем тестового клиента
client, _ = Client.objects.get_or_create(
email='test@example.com',
defaults={
'first_name': 'Тестовый',
'last_name': 'Клиент',
'phone_number': '+7-900-000-0000'
}
)
# Создаем или получаем тестовую услугу
service, _ = Service.objects.get_or_create(
name='Разработка сайта',
defaults={
'description': 'Профессиональная разработка веб-сайтов',
'price': 100000.00,
'category': cat_web
}
)
# Тестовые данные проектов
test_projects = [
{
'name': 'Корпоративный портал TechCorp',
'short_description': 'Современный корпоративный портал с системой управления документами, интеграцией с CRM и модулем HR.',
'description': '<p>Разработан комплексный корпоративный портал для компании TechCorp, включающий в себя систему управления документами, интеграцию с CRM-системой и модуль управления персоналом.</p><p>Основные функции: документооборот, календарь событий, внутренние новости, система заявок, интеграция с почтовыми сервисами.</p>',
'technologies': 'Django, PostgreSQL, Redis, Celery, Docker, React.js',
'duration': '4 месяца',
'team_size': 5,
'views_count': 1245,
'likes_count': 89,
'completion_date': date(2024, 8, 15),
'categories': [cat_web, cat_analytics],
'is_featured': True
},
{
'name': 'Мобильное приложение FoodDelivery',
'short_description': 'Cross-platform приложение для доставки еды с геолокацией, онлайн-платежами и системой рейтингов.',
'description': '<p>Создано мобильное приложение для службы доставки еды с поддержкой iOS и Android платформ.</p><p>Функционал включает: поиск ресторанов по геолокации, онлайн-заказы, интеграцию с платежными системами, отслеживание курьера в реальном времени, система рейтингов и отзывов.</p>',
'technologies': 'React Native, Node.js, MongoDB, Socket.io, Stripe API',
'duration': '6 месяцев',
'team_size': 4,
'views_count': 892,
'likes_count': 156,
'completion_date': date(2024, 10, 20),
'categories': [cat_mobile, cat_ecommerce],
'is_featured': False
},
{
'name': 'Аналитическая панель SmartMetrics',
'short_description': 'Интерактивная панель управления с визуализацией данных, машинным обучением и предиктивной аналитикой.',
'description': '<p>Разработана комплексная система аналитики для обработки больших данных с возможностями машинного обучения.</p><p>Включает: интерактивные дашборды, автоматизированные отчеты, прогнозирование трендов, интеграция с различными источниками данных, алгоритмы машинного обучения.</p>',
'technologies': 'Python, Django, PostgreSQL, Redis, TensorFlow, D3.js, Pandas',
'duration': '5 месяцев',
'team_size': 6,
'views_count': 673,
'likes_count': 124,
'completion_date': date(2024, 7, 10),
'categories': [cat_analytics, cat_web],
'is_featured': True
},
{
'name': 'E-commerce платформа ShopMaster',
'short_description': 'Полнофункциональная платформа интернет-торговли с многопользовательскими магазинами и системой управления.',
'description': '<p>Создана масштабируемая e-commerce платформа, поддерживающая множественные магазины на одной основе.</p><p>Возможности: многопользовательская архитектура, система платежей, управление складом, программы лояльности, мобильная оптимизация, SEO инструменты.</p>',
'technologies': 'Laravel, MySQL, Redis, Elasticsearch, Vue.js, Stripe, PayPal',
'duration': '8 месяцев',
'team_size': 7,
'views_count': 1567,
'likes_count': 203,
'completion_date': date(2024, 11, 5),
'categories': [cat_ecommerce, cat_web, cat_mobile],
'is_featured': True
},
{
'name': 'Дизайн-система BrandKit',
'short_description': 'Комплексная дизайн-система для финтех стартапа с фирменным стилем, UI-компонентами и брендбуком.',
'description': '<p>Разработана полная дизайн-система для финтех компании, включающая создание фирменного стиля, UI-компонентов и подробного брендбука.</p><p>Результат: логотип и фирменный стиль, библиотека UI-компонентов, руководство по использованию бренда, адаптация для различных платформ.</p>',
'technologies': 'Figma, Adobe Creative Suite, Principle, Sketch, InVision',
'duration': '3 месяца',
'team_size': 3,
'views_count': 445,
'likes_count': 78,
'completion_date': date(2024, 9, 30),
'categories': [cat_design],
'is_featured': False
}
]
print(f"Текущее количество проектов: {Project.objects.count()}")
# Создаем проекты
for i, project_data in enumerate(test_projects):
categories_to_add = project_data.pop('categories')
project, created = Project.objects.get_or_create(
name=project_data['name'],
defaults={
**project_data,
'client': client,
'service': service,
'status': 'completed',
'display_order': i + 1
}
)
if created:
# Добавляем категории
project.categories.set(categories_to_add)
print(f"✅ Создан проект: {project.name}")
else:
print(f"⚠️ Проект уже существует: {project.name}")
print(f"\nИтого проектов в базе: {Project.objects.count()}")
print(f"Завершенных проектов: {Project.objects.filter(status='completed').count()}")
print(f"Избранных проектов: {Project.objects.filter(is_featured=True).count()}")
if __name__ == '__main__':
create_test_projects()

350
preview.html Normal file
View File

@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Проект Django E-commerce - Предварительный просмотр</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css">
<link rel="stylesheet" href="smartsoltech/static/assets/css/modern-styles.css">
<link rel="stylesheet" href="smartsoltech/static/assets/css/compact-gallery.css">"
<style>
.main-content {
padding-top: 2rem;
padding-bottom: 3rem;
}
.content-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
</style>
</head>
<body>
<div class="main-content">
<div class="content-wrapper">
<!-- Компактная медиа-галерея -->
<div class="compact-media-gallery">
<div class="row g-3">
<!-- Основное изображение -->
<div class="col-lg-8">
<div class="main-media-display">
<div class="main-media-item" id="main-media">
<a href="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" data-lightbox="project-gallery" data-title="Главное изображение проекта">
<img src="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" alt="Главное изображение" class="main-media-img">
</a>
</div>
</div>
</div>
<!-- Сетка превью -->
<div class="col-lg-4">
<div class="media-thumbnails-grid">
<div class="thumbnail-item active" data-index="0">
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение', 'image', 'Главное изображение проекта')">
<img src="https://via.placeholder.com/200x200/4f46e5/ffffff?text=Превью+1" alt="Превью 1" class="thumbnail-img">
<div class="thumbnail-overlay">
<i class="fas fa-search-plus"></i>
</div>
</div>
<a href="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение" data-lightbox="project-gallery" data-title="Главное изображение проекта" style="display: none;"></a>
</div>
<div class="thumbnail-item" data-index="1">
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/7c3aed/ffffff?text=Скриншот+1', 'image', 'Скриншот интерфейса')">
<img src="https://via.placeholder.com/200x200/7c3aed/ffffff?text=Превью+2" alt="Превью 2" class="thumbnail-img">
<div class="thumbnail-overlay">
<i class="fas fa-search-plus"></i>
</div>
</div>
<a href="https://via.placeholder.com/800x500/7c3aed/ffffff?text=Скриншот+1" data-lightbox="project-gallery" data-title="Скриншот интерфейса" style="display: none;"></a>
</div>
<div class="thumbnail-item" data-index="2">
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/f59e0b/ffffff?text=Скриншот+2', 'image', 'Мобильная версия')">
<img src="https://via.placeholder.com/200x200/f59e0b/ffffff?text=Превью+3" alt="Превью 3" class="thumbnail-img">
<div class="thumbnail-overlay">
<i class="fas fa-search-plus"></i>
</div>
</div>
<a href="https://via.placeholder.com/800x500/f59e0b/ffffff?text=Скриншот+2" data-lightbox="project-gallery" data-title="Мобильная версия" style="display: none;"></a>
</div>
<div class="thumbnail-item" data-index="3">
<div class="thumbnail-wrapper" onclick="switchMainMedia('https://via.placeholder.com/800x500/10b981/ffffff?text=Админ+панель', 'image', 'Административная панель')">
<img src="https://via.placeholder.com/200x200/10b981/ffffff?text=Превью+4" alt="Превью 4" class="thumbnail-img">
<div class="thumbnail-overlay">
<i class="fas fa-search-plus"></i>
</div>
</div>
<a href="https://via.placeholder.com/800x500/10b981/ffffff?text=Админ+панель" data-lightbox="project-gallery" data-title="Административная панель" style="display: none;"></a>
</div>
</div>
</div>
</div>
</div>
<!-- Основной контент - двухколоночная структура -->
<div class="row">
<!-- Левая колонка - описание проекта -->
<div class="col-lg-8">
<div class="project-content">
<h2 class="mb-4">Описание проекта</h2>
<div class="description-text">
<p>Этот проект представляет собой <strong>современное веб-приложение</strong> электронной коммерции, разработанное с использованием Django и современных технологий фронтенда.</p>
<h3>Ключевые особенности</h3>
<ul>
<li><em>Адаптивный дизайн</em>, оптимизированный для всех устройств</li>
<li>Интеграция с <a href="#">популярными платежными системами</a></li>
<li>Многоуровневая система категорий товаров</li>
<li>Расширенная система поиска и фильтрации</li>
<li>Административная панель для управления контентом</li>
</ul>
<blockquote>
Проект демонстрирует современные подходы к веб-разработке, включая использование микросервисной архитектуры, контейнеризации и непрерывной интеграции.
</blockquote>
<h4>Технические детали</h4>
<p>Для обеспечения высокой производительности использовались следующие решения:</p>
<ol>
<li><code>Redis</code> для кеширования данных</li>
<li><code>PostgreSQL</code> как основная база данных</li>
<li><code>Docker</code> для контейнеризации</li>
</ol>
<hr>
<p><strong>Результат:</strong> Платформа способна обрабатывать <em>более 10,000 одновременных пользователей</em> с временем отклика менее 200ms.</p>
</div>
</div>
</div>
<!-- Правая колонка - технологии -->
<div class="col-lg-4">
<div class="tech-sidebar-section">
<h3 class="tech-sidebar-title">Технологии</h3>
<div class="technologies-html-content">
<p><code>Python</code> <code>Django</code> <code>PostgreSQL</code></p>
<p><code>JavaScript</code> <code>HTML5</code> <code>CSS3</code></p>
<p><code>Docker</code> <code>Redis</code> <code>Bootstrap</code></p>
<p><strong>Дополнительно:</strong> <code>Bash</code> <code>SQLite3</code></p>
</div>
</div>
</div>
</div>
<!-- Дополнительная секция -->
<div class="row mt-5">
<div class="col-12">
<div class="additional-info p-4 rounded-4" style="background: #f8fafc; border: 1px solid #e2e8f0;">
<h3 class="mb-3">Результаты проекта</h3>
<div class="row g-4">
<div class="col-md-3 text-center">
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">150%</div>
<div class="stat-label">Рост продаж</div>
</div>
<div class="col-md-3 text-center">
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">2.5x</div>
<div class="stat-label">Быстрее загрузка</div>
</div>
<div class="col-md-3 text-center">
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">95%</div>
<div class="stat-label">Доступность</div>
</div>
<div class="col-md-3 text-center">
<div class="stat-number" style="font-size: 2rem; font-weight: bold; color: #4f46e5;">100%</div>
<div class="stat-label">Адаптивность</div>
</div>
</div>
</div>
</div>
</div>
<!-- Карусель похожих проектов -->
<div class="similar-projects-section">
<div class="container">
<h2 class="section-title">Похожие проекты</h2>
<div class="similar-projects-carousel">
<div class="swiper similarSwiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
<div class="similar-project-card">
<div class="similar-thumb">
<img src="https://via.placeholder.com/300x200/4f46e5/ffffff?text=Проект+1" alt="Проект 1">
<div class="project-overlay">
<i class="fas fa-external-link-alt"></i>
</div>
</div>
<div class="similar-content">
<h4 class="project-title">E-commerce платформа</h4>
<p class="project-description">Современная платформа для онлайн-торговли с интеграцией платежных систем</p>
<div class="project-categories">
<span class="category-tag">Web</span>
<span class="category-tag">Django</span>
</div>
</div>
</div>
</div>
<div class="swiper-slide">
<div class="similar-project-card">
<div class="similar-thumb">
<img src="https://via.placeholder.com/300x200/7c3aed/ffffff?text=Проект+2" alt="Проект 2">
<div class="project-overlay">
<i class="fas fa-external-link-alt"></i>
</div>
</div>
<div class="similar-content">
<h4 class="project-title">CRM система</h4>
<p class="project-description">Система управления клиентскими отношениями с аналитикой</p>
<div class="project-categories">
<span class="category-tag">CRM</span>
<span class="category-tag">Analytics</span>
</div>
</div>
</div>
</div>
<div class="swiper-slide">
<div class="similar-project-card">
<div class="similar-thumb">
<img src="https://via.placeholder.com/300x200/f59e0b/ffffff?text=Проект+3" alt="Проект 3">
<div class="project-overlay">
<i class="fas fa-external-link-alt"></i>
</div>
</div>
<div class="similar-content">
<h4 class="project-title">Мобильное приложение</h4>
<p class="project-description">iOS и Android приложение для управления задачами</p>
<div class="project-categories">
<span class="category-tag">Mobile</span>
<span class="category-tag">React Native</span>
</div>
</div>
</div>
</div>
<div class="swiper-slide">
<div class="similar-project-card">
<div class="similar-thumb">
<div class="image-placeholder">
<div class="placeholder-content">
<i class="fas fa-image placeholder-icon"></i>
<div class="placeholder-text">Проект без изображения</div>
</div>
</div>
<div class="project-overlay">
<i class="fas fa-external-link-alt"></i>
</div>
</div>
<div class="similar-content">
<h4 class="project-title">Analytics Dashboard</h4>
<p class="project-description">Интерактивная панель аналитики с визуализацией данных</p>
<div class="project-categories">
<span class="category-tag">Analytics</span>
<span class="category-tag">D3.js</span>
</div>
</div>
</div>
</div>
</div>
<div class="swiper-button-next similar-next"></div>
<div class="swiper-button-prev similar-prev"></div>
<div class="swiper-pagination similar-pagination"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js"></script>
<script>
// Функция переключения основного медиа
function switchMainMedia(url, type, caption, poster = '') {
const mainMediaContainer = document.getElementById('main-media');
if (!mainMediaContainer) return;
// Очищаем контейнер
mainMediaContainer.innerHTML = '';
if (type === 'image') {
const link = document.createElement('a');
link.href = url;
link.setAttribute('data-lightbox', 'project-gallery');
link.setAttribute('data-title', caption);
const img = document.createElement('img');
img.src = url;
img.alt = caption;
img.className = 'main-media-img';
link.appendChild(img);
mainMediaContainer.appendChild(link);
} else if (type === 'video') {
const video = document.createElement('video');
video.controls = true;
video.className = 'main-media-video';
if (poster) {
video.poster = poster;
}
const source = document.createElement('source');
source.src = url;
source.type = 'video/mp4';
video.appendChild(source);
video.appendChild(document.createTextNode('Ваш браузер не поддерживает видео.'));
mainMediaContainer.appendChild(video);
}
// Обновляем активный thumbnail
document.querySelectorAll('.thumbnail-item').forEach(item => item.classList.remove('active'));
event.target.closest('.thumbnail-item').classList.add('active');
}
// Инициализация Swiper для карусели
const swiper = new Swiper('.similarSwiper', {
effect: 'coverflow',
grabCursor: true,
centeredSlides: true,
slidesPerView: 'auto',
spaceBetween: 40,
loop: true,
coverflowEffect: {
rotate: 15,
stretch: 0,
depth: 200,
modifier: 1.5,
slideShadows: true,
},
navigation: {
nextEl: '.similar-next',
prevEl: '.similar-prev',
},
pagination: {
el: '.similar-pagination',
clickable: true,
},
breakpoints: {
768: {
slidesPerView: 2,
spaceBetween: 20,
},
1024: {
slidesPerView: 3,
spaceBetween: 30,
}
}
});
</script>
</body>
</html>

73
reset_database.sql Normal file
View File

@@ -0,0 +1,73 @@
-- Скрипт для полной очистки базы данных smartsoltech_db
-- ВНИМАНИЕ: Этот скрипт удалит ВСЕ данные из базы данных!
-- Отключаем проверку внешних ключей
SET session_replication_role = replica;
-- Удаляем все таблицы Django приложений
DROP TABLE IF EXISTS django_migrations CASCADE;
DROP TABLE IF EXISTS django_content_type CASCADE;
DROP TABLE IF EXISTS auth_permission CASCADE;
DROP TABLE IF EXISTS auth_group CASCADE;
DROP TABLE IF EXISTS auth_group_permissions CASCADE;
DROP TABLE IF EXISTS auth_user CASCADE;
DROP TABLE IF EXISTS auth_user_groups CASCADE;
DROP TABLE IF EXISTS auth_user_user_permissions CASCADE;
DROP TABLE IF EXISTS django_admin_log CASCADE;
DROP TABLE IF EXISTS django_session CASCADE;
-- Удаляем таблицы приложения web
DROP TABLE IF EXISTS web_herobanner CASCADE;
DROP TABLE IF EXISTS web_category CASCADE;
DROP TABLE IF EXISTS web_service CASCADE;
DROP TABLE IF EXISTS web_client CASCADE;
DROP TABLE IF EXISTS web_order CASCADE;
DROP TABLE IF EXISTS web_project CASCADE;
DROP TABLE IF EXISTS web_project_categories CASCADE;
DROP TABLE IF EXISTS web_projectmedia CASCADE;
DROP TABLE IF EXISTS web_portfolioitem CASCADE;
DROP TABLE IF EXISTS web_portfolioitem_categories CASCADE;
DROP TABLE IF EXISTS web_portfoliocategory CASCADE;
DROP TABLE IF EXISTS web_portfoliomedia CASCADE;
DROP TABLE IF EXISTS web_review CASCADE;
DROP TABLE IF EXISTS web_blogpost CASCADE;
DROP TABLE IF EXISTS web_servicerequest CASCADE;
DROP TABLE IF EXISTS web_contactinfo CASCADE;
DROP TABLE IF EXISTS web_team CASCADE;
DROP TABLE IF EXISTS web_career CASCADE;
DROP TABLE IF EXISTS web_newspost CASCADE;
-- Удаляем таблицы приложения comunication
DROP TABLE IF EXISTS comunication_usercommunication CASCADE;
DROP TABLE IF EXISTS comunication_emailsettings CASCADE;
DROP TABLE IF EXISTS comunication_telegramsettings CASCADE;
-- Удаляем все последовательности (sequences)
DROP SEQUENCE IF EXISTS web_herobanner_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_category_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_service_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_client_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_order_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_project_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_project_categories_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_projectmedia_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_portfolioitem_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_portfolioitem_categories_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_portfoliocategory_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_portfoliomedia_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_review_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_blogpost_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_servicerequest_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_contactinfo_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_team_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_career_id_seq CASCADE;
DROP SEQUENCE IF EXISTS web_newspost_id_seq CASCADE;
DROP SEQUENCE IF EXISTS comunication_usercommunication_id_seq CASCADE;
DROP SEQUENCE IF EXISTS comunication_emailsettings_id_seq CASCADE;
DROP SEQUENCE IF EXISTS comunication_telegramsettings_id_seq CASCADE;
-- Включаем обратно проверку внешних ключей
SET session_replication_role = DEFAULT;
-- Выводим сообщение о завершении
SELECT 'База данных успешно очищена!' as status;

View File

@@ -0,0 +1,589 @@
/* Современная медиа-галерея */
.modern-media-gallery {
background: white;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08);
border: 1px solid #f1f5f9;
margin-bottom: 3rem;
}
/* Основное медиа */
.main-media-container {
position: relative;
aspect-ratio: 16/10;
overflow: hidden;
background: #f8fafc;
}
.main-media-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.main-media-item {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.5s ease;
}
.main-media-item.active {
opacity: 1;
}
.main-media-img,
.main-media-video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.embed-container {
position: relative;
width: 100%;
height: 100%;
}
.main-media-embed {
width: 100%;
height: 100%;
border: none;
}
/* Overlay с информацией */
.media-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 2rem;
color: white;
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
justify-content: space-between;
align-items: end;
}
.main-media-item:hover .media-overlay {
opacity: 1;
}
.media-info {
flex: 1;
}
.media-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.media-meta {
font-size: 0.9rem;
opacity: 0.8;
}
.media-action-btn {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
margin-left: 1rem;
}
.media-action-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
/* Навигационные кнопки */
.media-nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.9);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: none;
border-radius: 50%;
color: #4f46e5;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
cursor: pointer;
opacity: 0;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.main-media-wrapper:hover .media-nav-btn {
opacity: 1;
}
.prev-btn {
left: 20px;
}
.next-btn {
right: 20px;
}
.media-nav-btn:hover {
background: white;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
/* Миниатюры */
.thumbnails-container {
padding: 1.5rem;
background: #fafbfc;
border-top: 1px solid #f1f5f9;
}
.thumbnails-wrapper {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 8px;
}
/* Для Firefox */
.thumbnails-wrapper {
scrollbar-width: thin;
scrollbar-color: #cbd5e0 transparent;
}
/* Для Webkit браузеров */
.thumbnails-wrapper::-webkit-scrollbar {
height: 6px;
}
.thumbnails-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.thumbnails-wrapper::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 3px;
}
.thumbnail-item {
position: relative;
width: 80px;
height: 60px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
border: 2px solid transparent;
}
.thumbnail-item:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.thumbnail-item.active {
border-color: #4f46e5;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-thumbnail-placeholder,
.embed-thumbnail-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
}
.media-type-badge {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: white;
}
.media-type-badge.video {
background: #ef4444;
}
.media-type-badge.embed {
background: #06b6d4;
}
.thumbnail-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.thumbnail-item:hover .thumbnail-overlay {
opacity: 1;
}
.thumbnail-number {
color: white;
font-weight: 600;
font-size: 0.9rem;
}
/* Индикатор прогресса */
.gallery-progress {
height: 4px;
background: #f1f5f9;
position: relative;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%);
transition: width 0.4s ease;
border-radius: 2px;
}
/* Placeholder для проектов без изображений */
.project-placeholder-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #64748b;
text-align: center;
}
.project-placeholder-image i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.project-placeholder-image p {
font-size: 1.1rem;
font-weight: 500;
margin: 0;
opacity: 0.7;
}
/* Адаптивность */
@media (max-width: 1024px) {
.main-media-container {
aspect-ratio: 16/9;
}
.media-overlay {
padding: 1.5rem;
}
.thumbnails-container {
padding: 1rem;
}
.thumbnail-item {
width: 70px;
height: 52px;
}
}
@media (max-width: 768px) {
.modern-media-gallery {
border-radius: 16px;
margin-bottom: 2rem;
}
.media-overlay {
padding: 1rem;
background: rgba(0, 0, 0, 0.7);
opacity: 1;
}
.media-nav-btn {
opacity: 1;
width: 40px;
height: 40px;
font-size: 1rem;
}
.prev-btn {
left: 12px;
}
.next-btn {
right: 12px;
}
.thumbnails-container {
padding: 0.75rem;
}
.thumbnail-item {
width: 60px;
height: 45px;
}
.media-action-btn {
width: 40px;
height: 40px;
font-size: 1rem;
}
}
/* HTML-контент в описании проекта */
.description-text h1,
.description-text h2,
.description-text h3,
.description-text h4,
.description-text h5,
.description-text h6 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: 600;
color: #1a202c;
}
.description-text h1 { font-size: 2rem; }
.description-text h2 { font-size: 1.75rem; }
.description-text h3 { font-size: 1.5rem; }
.description-text h4 { font-size: 1.25rem; }
.description-text h5 { font-size: 1.1rem; }
.description-text h6 { font-size: 1rem; }
.description-text p {
margin-bottom: 1.2rem;
line-height: 1.8;
}
.description-text ul,
.description-text ol {
margin: 1.5rem 0;
padding-left: 2rem;
}
.description-text li {
margin-bottom: 0.5rem;
line-height: 1.7;
}
.description-text blockquote {
border-left: 4px solid #4f46e5;
padding-left: 1.5rem;
margin: 2rem 0;
font-style: italic;
background: #f8fafc;
padding: 1.5rem;
border-radius: 8px;
}
.description-text code {
background: #f1f5f9;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9em;
color: #e53e3e;
}
.description-text pre {
background: #1a202c;
color: #fff;
padding: 1.5rem;
border-radius: 8px;
overflow-x: auto;
margin: 1.5rem 0;
}
.description-text pre code {
background: none;
color: inherit;
padding: 0;
}
.description-text a {
color: #4f46e5;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.3s ease;
}
.description-text a:hover {
border-bottom-color: #4f46e5;
}
.description-text strong,
.description-text b {
font-weight: 600;
color: #1a202c;
}
.description-text em,
.description-text i {
font-style: italic;
}
.description-text img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.description-text hr {
border: none;
height: 2px;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
margin: 3rem 0;
border-radius: 1px;
}
.description-text table {
width: 100%;
border-collapse: collapse;
margin: 2rem 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.description-text th,
.description-text td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
.description-text th {
background: #f8fafc;
font-weight: 600;
color: #1a202c;
}
/* HTML-контент в технологиях */
.technologies-html-content {
line-height: 1.6;
}
.technologies-html-content p {
margin-bottom: 1rem;
}
.technologies-html-content code {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
font-weight: 500;
display: inline-block;
margin: 0.25rem 0;
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.2);
transition: all 0.3s ease;
}
.technologies-html-content code:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.technologies-html-content p code {
margin: 0.2rem;
white-space: nowrap;
}
.technologies-html-content h1,
.technologies-html-content h2,
.technologies-html-content h3,
.technologies-html-content h4,
.technologies-html-content h5,
.technologies-html-content h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
color: #1a202c;
}
.technologies-html-content ul,
.technologies-html-content ol {
margin: 1rem 0;
padding-left: 1.5rem;
}
.technologies-html-content li {
margin-bottom: 0.4rem;
}
.technologies-html-content strong,
.technologies-html-content b {
font-weight: 600;
color: #1a202c;
}
.technologies-html-content em,
.technologies-html-content i {
font-style: italic;
}
.technologies-html-content a {
color: #4f46e5;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.3s ease;
}
.technologies-html-content a:hover {
border-bottom-color: #4f46e5;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
// Modern Project Detail Page Enhancements
document.addEventListener('DOMContentLoaded', function() {
// Animate counter numbers
function animateCounters() {
const counters = document.querySelectorAll('.stat-number[data-target]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const counter = entry.target;
const target = parseInt(counter.dataset.target);
const duration = 2000; // 2 seconds
const step = target / (duration / 16); // 60fps
let current = 0;
const timer = setInterval(() => {
current += step;
counter.textContent = Math.floor(current);
if (current >= target) {
counter.textContent = target;
clearInterval(timer);
}
}, 16);
observer.unobserve(counter);
}
});
}, {
threshold: 0.5
});
counters.forEach(counter => observer.observe(counter));
}
// Scroll-triggered animations
function initScrollAnimations() {
const animatedElements = document.querySelectorAll('.content-section, .tech-item, .info-item');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}, index * 100);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
animatedElements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
}
// Share functionality
function initShareButton() {
const shareBtn = document.querySelector('.share-btn');
if (shareBtn) {
shareBtn.addEventListener('click', async function() {
const projectTitle = document.querySelector('.hero-title').textContent;
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: projectTitle,
text: `Посмотрите на этот проект: ${projectTitle}`,
url: url
});
} catch (err) {
console.log('Sharing failed:', err);
fallbackShare(url);
}
} else {
fallbackShare(url);
}
});
}
}
function fallbackShare(url) {
// Copy to clipboard as fallback
if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(() => {
showToast('Ссылка скопирована в буфер обмена!');
});
}
}
function showToast(message) {
// Create toast notification
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #48bb78, #38a169);
color: white;
padding: 1rem 1.5rem;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 10000;
transform: translateX(400px);
transition: transform 0.3s ease;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
toast.style.transform = 'translateX(400px)';
setTimeout(() => document.body.removeChild(toast), 300);
}, 3000);
}
// Tech item hover effects
function initTechInteractions() {
const techItems = document.querySelectorAll('.tech-item');
techItems.forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-8px) scale(1.02)';
});
item.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(-5px) scale(1)';
});
});
}
// Parallax effect for hero background
function initParallaxEffect() {
const heroBackground = document.querySelector('.hero-pattern');
if (!heroBackground) return;
let ticking = false;
function updateParallax() {
const scrolled = window.pageYOffset;
const rate = scrolled * -0.3;
heroBackground.style.transform = `translateY(${rate}px)`;
ticking = false;
}
function requestTick() {
if (!ticking) {
requestAnimationFrame(updateParallax);
ticking = true;
}
}
window.addEventListener('scroll', requestTick);
}
// Smooth scroll for internal links
function initSmoothScroll() {
const links = document.querySelectorAll('a[href^="#"]');
links.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetSection = document.querySelector(targetId);
if (targetSection) {
targetSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
}
// Initialize all enhancements
animateCounters();
initScrollAnimations();
initShareButton();
initTechInteractions();
initParallaxEffect();
initSmoothScroll();
// Add loading class removal after page load
window.addEventListener('load', function() {
document.body.classList.add('page-loaded');
});
});
// CSS for page loading animation
const loadingStyles = `
body:not(.page-loaded) .project-hero {
opacity: 0;
transform: translateY(50px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
body.page-loaded .project-hero {
opacity: 1;
transform: translateY(0);
}
.toast-notification {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 0.9rem;
font-weight: 500;
}
`;
// Inject loading styles
const styleSheet = document.createElement('style');
styleSheet.textContent = loadingStyles;
document.head.appendChild(styleSheet);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,8 +1,8 @@
# Generated by Django 5.1.1 on 2025-11-25 23:21 # Generated by Django 5.1.1 on 2025-11-25 23:21
import ckeditor_uploader.fields # Modified to remove ckeditor dependency
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='blogpost', model_name='blogpost',
name='content', name='content',
field=ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание'), field=models.TextField(verbose_name='Содержание'),
), ),
migrations.CreateModel( migrations.CreateModel(
name='NewsPost', name='NewsPost',
@@ -41,7 +41,7 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=200, verbose_name='Заголовок')), ('title', models.CharField(max_length=200, verbose_name='Заголовок')),
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')), ('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
('excerpt', models.TextField(max_length=300, verbose_name='Краткое описание')), ('excerpt', models.TextField(max_length=300, verbose_name='Краткое описание')),
('content', ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание')), ('content', models.TextField(verbose_name='Содержание')),
('featured_image', models.ImageField(upload_to='news/', verbose_name='Главное изображение')), ('featured_image', models.ImageField(upload_to='news/', verbose_name='Главное изображение')),
('tags', models.CharField(blank=True, help_text='Разделите запятыми', max_length=200, verbose_name='Теги')), ('tags', models.CharField(blank=True, help_text='Разделите запятыми', max_length=200, verbose_name='Теги')),
('is_published', models.BooleanField(default=False, verbose_name='Опубликовано')), ('is_published', models.BooleanField(default=False, verbose_name='Опубликовано')),
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=200, verbose_name='Название проекта')), ('title', models.CharField(max_length=200, verbose_name='Название проекта')),
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')), ('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
('short_description', models.TextField(max_length=300, verbose_name='Краткое описание')), ('short_description', models.TextField(max_length=300, verbose_name='Краткое описание')),
('description', ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Полное описание')), ('description', models.TextField(blank=True, verbose_name='Полное описание')),
('thumbnail', models.ImageField(upload_to='portfolio/thumbnails/', verbose_name='Превью изображение')), ('thumbnail', models.ImageField(upload_to='portfolio/thumbnails/', verbose_name='Превью изображение')),
('client', models.CharField(blank=True, max_length=200, verbose_name='Клиент')), ('client', models.CharField(blank=True, max_length=200, verbose_name='Клиент')),
('project_url', models.URLField(blank=True, verbose_name='Ссылка на проект')), ('project_url', models.URLField(blank=True, verbose_name='Ссылка на проект')),

View File

@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='project', model_name='project',
name='categories', name='categories',
field=models.ManyToManyField(blank=True, related_name='projects', to='web.projectcategory', verbose_name='Категории'), field=models.ManyToManyField(blank=True, related_name='projects', to='web.category', verbose_name='Категории'),
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProjectMedia', name='ProjectMedia',

View File

@@ -38,11 +38,8 @@ class Migration(migrations.Migration):
name='slug', name='slug',
field=models.SlugField(blank=True, max_length=100, null=True, unique=True, verbose_name='URL'), field=models.SlugField(blank=True, max_length=100, null=True, unique=True, verbose_name='URL'),
), ),
migrations.AlterField( # Удаляем проблемную операцию изменения ManyToManyField
model_name='project', # Поле уже существует с нужными параметрами
name='categories',
field=models.ManyToManyField(blank=True, related_name='projects', to='web.category', verbose_name='Категории'),
),
migrations.AlterField( migrations.AlterField(
model_name='projectmedia', model_name='projectmedia',
name='project', name='project',

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.1 on 2025-11-26 01:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0016_delete_projectcategory_alter_category_options_and_more'),
]
operations = [
]

View File

@@ -0,0 +1,47 @@
# Fix for column name in project categories table
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0017_auto_20251126_0146'),
]
operations = [
migrations.RunSQL(
# Forward SQL - rename column and fix constraints
"""
-- Rename the column if it still exists as projectcategory_id
DO $$
BEGIN
IF EXISTS (
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'web_project_categories'
AND column_name = 'projectcategory_id'
) THEN
ALTER TABLE web_project_categories RENAME COLUMN projectcategory_id TO category_id;
-- Add foreign key constraint if it doesn't exist
IF NOT EXISTS (
SELECT constraint_name
FROM information_schema.table_constraints
WHERE table_name = 'web_project_categories'
AND constraint_name = 'web_project_categories_category_id_fk'
) THEN
ALTER TABLE web_project_categories
ADD CONSTRAINT web_project_categories_category_id_fk
FOREIGN KEY (category_id) REFERENCES web_category(id) ON DELETE CASCADE;
END IF;
END IF;
END $$;
""",
# Reverse SQL
"""
ALTER TABLE web_project_categories RENAME COLUMN category_id TO projectcategory_id;
ALTER TABLE web_project_categories DROP CONSTRAINT IF EXISTS web_project_categories_category_id_fk;
"""
),
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.1 on 2025-11-26 10:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0018_fix_project_categories_column'),
]
operations = [
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.1 on 2025-11-26 10:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0019_add_project_slug'),
]
operations = [
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.1.1 on 2025-11-26 10:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0020_recreate_project_table'),
]
operations = [
]

View File

@@ -0,0 +1,322 @@
{% extends 'web/base_modern.html' %}
{% load static %}
{% block title %}Блог - SmartSolTech{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="hero-modern">
<div class="container-modern">
<div class="row justify-content-center text-center">
<div class="col-lg-8">
<div class="hero-content">
<h1 class="display-4 fw-bold mb-4">
<span class="text-gradient">Блог</span> SmartSolTech
</h1>
<p class="lead text-muted mb-5">
Новости, статьи и инсайты из мира IT и технологий
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Blog Posts Section -->
<section class="section-padding">
<div class="container-modern">
{% if blog_posts %}
<div class="row g-4">
{% for post in blog_posts %}
<div class="col-lg-4 col-md-6">
<article class="card-modern h-100 hover-lift">
<!-- Post Image/Video -->
{% if post.video %}
<div style="height: 250px; overflow: hidden; border-radius: 15px 15px 0 0; position: relative;">
<video class="w-100 h-100"
style="object-fit: cover;"
muted
{% if post.video_poster %}poster="{{ post.video_poster.url }}"{% endif %}>
<source src="{{ post.video.url }}" type="video/mp4">
{% if post.image %}
<img src="{{ post.image.url }}" class="w-100 h-100" style="object-fit: cover;" alt="{{ post.title }}">
{% endif %}
</video>
<div class="position-absolute top-0 end-0 p-3">
<span class="badge bg-primary">
<i class="fas fa-play"></i> Видео
</span>
</div>
<div class="position-absolute bottom-0 start-0 end-0 p-3" style="background: linear-gradient(transparent, rgba(0,0,0,0.7));">
<button class="btn btn-light btn-sm" onclick="this.closest('div').previousElementSibling.play()">
<i class="fas fa-play me-1"></i> Воспроизвести
</button>
</div>
</div>
{% elif post.image %}
<div style="height: 250px; overflow: hidden; border-radius: 15px 15px 0 0;">
<img src="{{ post.image.url }}" alt="{{ post.title }}"
class="w-100 h-100"
style="object-fit: cover;"
loading="lazy">
</div>
{% else %}
<div class="w-100 bg-gradient d-flex align-items-center justify-content-center"
style="height: 250px; border-radius: 15px 15px 0 0;">
<i class="fas fa-newspaper text-white" style="font-size: 3rem; opacity: 0.7;"></i>
</div>
{% endif %}
<!-- Post Content -->
<div class="card-body d-flex flex-column">
<div class="mb-3">
<span class="badge bg-primary-modern">
<i class="fas fa-newspaper me-1"></i>
Блог
</span>
</div>
<h3 class="h5 mb-3 text-dark">
<a href="{% url 'blog_post_detail' post.pk %}"
class="text-decoration-none text-dark hover-primary">
{{ post.title }}
</a>
</h3>
<div class="text-muted mb-3 flex-grow-1">
{% if post.content %}
<p>{{ post.content|striptags|truncatewords:20 }}</p>
{% else %}
<p>Нет описания...</p>
{% endif %}
</div>
<!-- Post Meta -->
<div class="d-flex justify-content-between align-items-center mt-auto pt-3 border-top">
<small class="text-muted">
<i class="fas fa-calendar-alt me-1"></i>
{{ post.published_date|date:"d.m.Y" }}
</small>
<a href="{% url 'blog_post_detail' post.pk %}"
class="btn btn-sm btn-outline-primary">
Читать далее
<i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</article>
</div>
{% endfor %}
</div>
<!-- Pagination (если планируется) -->
<div class="text-center mt-5">
<div class="d-inline-flex align-items-center gap-3">
<span class="text-muted">Показано {{ blog_posts.count }} из {{ blog_posts.count }} постов</span>
</div>
</div>
{% else %}
<!-- Empty State -->
<div class="text-center py-5">
<div class="empty-state">
<div class="mb-4">
<i class="fas fa-newspaper text-muted" style="font-size: 4rem;"></i>
</div>
<h3 class="h4 mb-3">Пока нет постов в блоге</h3>
<p class="text-muted mb-4">
Мы работаем над созданием интересного контента для вас
</p>
<a href="{% url 'home' %}" class="btn btn-primary-modern">
<i class="fas fa-home me-2"></i>
На главную
</a>
</div>
</div>
{% endif %}
</div>
</section>
<!-- Newsletter Section -->
<section class="section-padding bg-light">
<div class="container-modern">
<div class="row justify-content-center">
<div class="col-lg-6 text-center">
<div class="newsletter-cta">
<div class="mb-4">
<i class="fas fa-envelope text-primary" style="font-size: 3rem;"></i>
</div>
<h2 class="h3 mb-3">Следите за новостями</h2>
<p class="text-muted mb-4">
Подпишитесь на наши обновления, чтобы получать последние новости и статьи
</p>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<a href="mailto:info@smartsoltech.kr" class="btn btn-primary-modern">
<i class="fas fa-envelope me-2"></i>
Связаться с нами
</a>
<a href="{% url 'services' %}" class="btn btn-outline-primary">
<i class="fas fa-cogs me-2"></i>
Наши услуги
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extra_scripts %}
<script>
// Smooth scrolling for blog navigation
document.addEventListener('DOMContentLoaded', function() {
// Video play on hover
const videoCards = document.querySelectorAll('video');
videoCards.forEach(video => {
video.addEventListener('mouseenter', function() {
this.currentTime = 0;
this.play().catch(e => console.log('Video autoplay prevented'));
});
video.addEventListener('mouseleave', function() {
this.pause();
});
});
// Enhanced card hover effects
const cards = document.querySelectorAll('.hover-lift');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-10px)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
});
// Search functionality (if needed later)
function searchBlogPosts(query) {
// Future implementation for blog search
console.log('Searching for:', query);
}
</script>
<style>
.hover-lift {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.hover-lift:hover {
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
}
.hover-primary:hover {
color: var(--primary-color) !important;
}
.card-modern article {
border: none;
}
.empty-state {
max-width: 400px;
margin: 0 auto;
}
.newsletter-cta {
background: white;
padding: 2rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
/* Blog post content styling */
.text-muted p {
line-height: 1.6;
}
/* Video overlay improvements */
video {
transition: transform 0.3s ease;
}
video:hover {
transform: scale(1.05);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.hero-modern .display-4 {
font-size: 2rem;
}
.newsletter-cta {
margin: 0 1rem;
}
.d-flex.justify-content-center.gap-3 {
flex-direction: column;
align-items: center;
}
.d-flex.justify-content-center.gap-3 .btn {
width: 100%;
max-width: 250px;
}
}
/* Print styles */
@media print {
.newsletter-cta,
.btn {
display: none;
}
}
/* Enhanced accessibility */
@media (prefers-reduced-motion: reduce) {
.hover-lift,
video {
transition: none;
}
video:hover {
transform: none;
}
.hover-lift:hover {
transform: none;
}
}
/* Focus styles for better accessibility */
.hover-primary:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* Loading states */
.card-modern img {
transition: opacity 0.3s ease;
}
.card-modern img:not([src]) {
opacity: 0;
}
/* Enhanced video controls */
.position-absolute .btn:hover {
transform: scale(1.1);
}
/* Custom scrollbar for mobile */
@media (max-width: 768px) {
.container-modern {
padding-left: 1rem;
padding-right: 1rem;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,399 @@
{% extends 'web/base_modern.html' %}
{% load static %}
{% block title %}{{ blog_post.title }} - SmartSolTech{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="hero-modern">
<div class="container-modern">
<div class="row gy-4 align-items-center">
<div class="col-lg-8 mx-auto text-center">
<div class="blog-header">
<div class="mb-3">
<span class="badge bg-primary-modern">
<i class="fas fa-newspaper me-1"></i>
Блог
</span>
</div>
<h1 class="display-5 fw-bold mb-4">{{ blog_post.title }}</h1>
<div class="blog-meta d-flex justify-content-center align-items-center flex-wrap gap-3 text-muted">
<div class="d-flex align-items-center">
<i class="fas fa-calendar-alt me-2"></i>
<span>{{ blog_post.published_date|date:"d.m.Y" }}</span>
</div>
<div class="d-flex align-items-center">
<i class="fas fa-clock me-2"></i>
<span>{{ blog_post.published_date|date:"H:i" }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Blog Content -->
<section class="section-padding">
<div class="container-modern">
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Featured Image/Video -->
{% if blog_post.video %}
<div class="mb-5">
<video class="w-100 rounded-4 shadow-lg"
style="max-height: 500px; object-fit: cover;"
controls
{% if blog_post.video_poster %}poster="{{ blog_post.video_poster.url }}"{% endif %}>
<source src="{{ blog_post.video.url }}" type="video/mp4">
{% if blog_post.image %}
<!-- Fallback image if video not supported -->
<img src="{{ blog_post.image.url }}" class="w-100 rounded-4 shadow-lg" style="max-height: 500px; object-fit: cover;" alt="{{ blog_post.title }}">
{% endif %}
Ваш браузер не поддерживает видео.
</video>
</div>
{% elif blog_post.image %}
<div class="mb-5">
<img class="w-100 rounded-4 shadow-lg"
style="max-height: 500px; object-fit: cover;"
src="{{ blog_post.image.url }}"
alt="{{ blog_post.title }}" />
</div>
{% endif %}
<!-- Blog Content -->
<div class="blog-content">
<div class="content-wrapper">
{{ blog_post.content|safe }}
</div>
</div>
<!-- Blog Navigation -->
<div class="blog-navigation mt-5 pt-4 border-top">
<div class="row align-items-center">
<div class="col-auto">
<a href="{% url 'home' %}#blog" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>
Вернуться к блогу
</a>
</div>
<div class="col text-end">
<!-- Social Share Buttons -->
<div class="share-buttons">
<span class="text-muted me-3">Поделиться:</span>
<a href="javascript:void(0)"
onclick="shareToSocial('telegram')"
class="btn btn-sm btn-outline-primary me-2"
title="Поделиться в Telegram">
<i class="fab fa-telegram"></i>
</a>
<a href="javascript:void(0)"
onclick="shareToSocial('whatsapp')"
class="btn btn-sm btn-outline-success me-2"
title="Поделиться в WhatsApp">
<i class="fab fa-whatsapp"></i>
</a>
<a href="javascript:void(0)"
onclick="shareToSocial('copy')"
class="btn btn-sm btn-outline-secondary"
title="Скопировать ссылку">
<i class="fas fa-copy"></i>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<div class="blog-sidebar">
<!-- Contact CTA -->
<div class="card-modern mb-4">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-comments text-primary" style="font-size: 2rem;"></i>
</div>
<h5 class="mb-3">Есть вопросы?</h5>
<p class="text-muted mb-4">
Свяжитесь с нами для бесплатной консультации
</p>
<a href="mailto:info@smartsoltech.kr" class="btn btn-primary-modern">
<i class="fas fa-envelope me-2"></i>
Написать нам
</a>
</div>
</div>
<!-- Services CTA -->
<div class="card-modern">
<div class="card-body">
<h6 class="mb-3">
<i class="fas fa-cogs text-primary me-2"></i>
Наши услуги
</h6>
<div class="d-grid gap-2">
<a href="{% url 'services' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-laptop-code me-2"></i>
Веб-разработка
</a>
<a href="{% url 'services' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-mobile-alt me-2"></i>
Мобильные приложения
</a>
<a href="{% url 'services' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-chart-line me-2"></i>
IT консалтинг
</a>
<a href="{% url 'services' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-cloud me-2"></i>
Облачные решения
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section-padding bg-gradient text-white">
<div class="container-modern text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h2 class="display-6 fw-bold mb-4">
Готовы обсудить ваш проект?
</h2>
<p class="lead mb-5 opacity-90">
Получите бесплатную консультацию от наших экспертов
</p>
<div class="d-flex flex-wrap gap-3 justify-content-center">
<a href="mailto:info@smartsoltech.kr" class="btn btn-light btn-lg text-primary">
<i class="fas fa-envelope me-2"></i>
Связаться с нами
</a>
<a href="{% url 'services' %}" class="btn btn-outline-light btn-lg">
<i class="fas fa-th-large me-2"></i>
Наши услуги
</a>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extra_scripts %}
<script>
// Social sharing functionality
function shareToSocial(platform) {
const url = encodeURIComponent(window.location.href);
const title = encodeURIComponent('{{ blog_post.title|addslashes }}');
const text = encodeURIComponent('{{ blog_post.title|addslashes }} - SmartSolTech');
let shareUrl = '';
switch(platform) {
case 'telegram':
shareUrl = `https://t.me/share/url?url=${url}&text=${text}`;
break;
case 'whatsapp':
shareUrl = `https://wa.me/?text=${text}%20${url}`;
break;
case 'copy':
navigator.clipboard.writeText(window.location.href).then(() => {
// Show success message
const btn = event.target.closest('a');
const originalIcon = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
btn.classList.add('btn-success');
btn.classList.remove('btn-outline-secondary');
setTimeout(() => {
btn.innerHTML = originalIcon;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
return;
}
if (shareUrl) {
window.open(shareUrl, '_blank', 'width=550,height=420');
}
}
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
<style>
.blog-header {
margin-bottom: 2rem;
}
.blog-meta {
font-size: 0.9rem;
}
.blog-content {
line-height: 1.8;
font-size: 1.1rem;
}
.blog-content h1,
.blog-content h2,
.blog-content h3,
.blog-content h4,
.blog-content h5,
.blog-content h6 {
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.blog-content p {
margin-bottom: 1.5rem;
color: var(--text-secondary);
}
.blog-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
}
.blog-content blockquote {
border-left: 4px solid var(--primary-color);
background: var(--bg-light);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 8px 8px 0;
font-style: italic;
}
.blog-content ul,
.blog-content ol {
margin-bottom: 1.5rem;
padding-left: 2rem;
}
.blog-content li {
margin-bottom: 0.5rem;
}
.blog-content pre {
background: var(--bg-dark);
color: var(--text-light);
padding: 1.5rem;
border-radius: 8px;
overflow-x: auto;
margin: 1.5rem 0;
}
.blog-content code {
background: var(--bg-light);
color: var(--primary-color);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.blog-content pre code {
background: none;
color: inherit;
padding: 0;
}
.blog-sidebar {
position: sticky;
top: 2rem;
}
.share-buttons a {
transition: transform 0.2s ease;
}
.share-buttons a:hover {
transform: translateY(-2px);
}
.blog-navigation {
background: var(--bg-light);
padding: 1.5rem;
border-radius: 12px;
}
/* Content typography improvements */
.content-wrapper {
font-family: 'Georgia', serif;
}
.content-wrapper h1 { font-size: 2.5rem; }
.content-wrapper h2 { font-size: 2rem; }
.content-wrapper h3 { font-size: 1.5rem; }
.content-wrapper h4 { font-size: 1.25rem; }
.content-wrapper h5 { font-size: 1.1rem; }
.content-wrapper h6 { font-size: 1rem; }
/* Responsive improvements */
@media (max-width: 768px) {
.blog-header h1 {
font-size: 2rem;
}
.blog-meta {
font-size: 0.8rem;
}
.share-buttons {
margin-top: 1rem;
}
.share-buttons span {
display: block;
margin-bottom: 0.5rem;
}
.blog-sidebar {
position: relative;
top: auto;
margin-top: 2rem;
}
.content-wrapper h1 { font-size: 1.8rem; }
.content-wrapper h2 { font-size: 1.5rem; }
.content-wrapper h3 { font-size: 1.3rem; }
}
/* Print styles */
@media print {
.blog-navigation,
.blog-sidebar,
.share-buttons {
display: none;
}
.blog-content {
font-size: 12pt;
line-height: 1.5;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Предпросмотр современной галереи</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Lightbox2 CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css">
<!-- Наши стили -->
<link href="../../static/assets/css/compact-gallery.css" rel="stylesheet">
<style>
body {
background: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 2rem 0;
}
.container {
max-width: 1200px;
}
.preview-header {
text-align: center;
margin-bottom: 3rem;
}
.preview-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1rem;
}
.preview-header p {
font-size: 1.2rem;
color: #64748b;
}
</style>
</head>
<body>
<div class="container">
<div class="preview-header">
<h1>Современная медиа-галерея</h1>
<p>Интерактивная галерея с навигацией, миниатюрами и полноэкранным просмотром</p>
</div>
<!-- Современная медиа-галерея -->
<div class="modern-media-gallery">
<!-- Основное медиа -->
<div class="main-media-container">
<div class="main-media-wrapper">
<!-- Медиа элементы -->
<div class="main-media-item active" data-index="0">
<img src="https://via.placeholder.com/800x500/4f46e5/ffffff?text=Главное+изображение+1"
alt="Изображение 1" class="main-media-img">
<div class="media-overlay">
<div class="media-info">
<div class="media-title">Название изображения</div>
<div class="media-meta">Фото • 1920x1080 • 2.3 MB</div>
</div>
<button class="media-action-btn" onclick="openLightbox(0)">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<div class="main-media-item" data-index="1">
<img src="https://via.placeholder.com/800x500/7c3aed/ffffff?text=Главное+изображение+2"
alt="Изображение 2" class="main-media-img">
<div class="media-overlay">
<div class="media-info">
<div class="media-title">Второе изображение</div>
<div class="media-meta">Фото • 1920x1080 • 1.8 MB</div>
</div>
<button class="media-action-btn" onclick="openLightbox(1)">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<div class="main-media-item" data-index="2">
<video class="main-media-video" controls>
<source src="https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4" type="video/mp4">
</video>
<div class="media-overlay">
<div class="media-info">
<div class="media-title">Демо видео</div>
<div class="media-meta">Видео • MP4 • 1:32</div>
</div>
<button class="media-action-btn" onclick="toggleVideo(2)">
<i class="fas fa-play"></i>
</button>
</div>
</div>
<div class="main-media-item" data-index="3">
<div class="embed-container">
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
class="main-media-embed"
allowfullscreen></iframe>
</div>
<div class="media-overlay">
<div class="media-info">
<div class="media-title">YouTube видео</div>
<div class="media-meta">Встраивание • YouTube</div>
</div>
<button class="media-action-btn" onclick="openFullscreen(3)">
<i class="fas fa-external-link-alt"></i>
</button>
</div>
</div>
<div class="main-media-item" data-index="4">
<img src="https://via.placeholder.com/800x500/06b6d4/ffffff?text=Главное+изображение+3"
alt="Изображение 3" class="main-media-img">
<div class="media-overlay">
<div class="media-info">
<div class="media-title">Третье изображение</div>
<div class="media-meta">Фото • 1920x1080 • 3.1 MB</div>
</div>
<button class="media-action-btn" onclick="openLightbox(4)">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<!-- Навигационные кнопки -->
<button class="media-nav-btn prev-btn" onclick="gallery.previousMedia()">
<i class="fas fa-chevron-left"></i>
</button>
<button class="media-nav-btn next-btn" onclick="gallery.nextMedia()">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<!-- Индикатор прогресса -->
<div class="gallery-progress">
<div class="progress-bar" style="width: 20%"></div>
</div>
<!-- Миниатюры -->
<div class="thumbnails-container">
<div class="thumbnails-wrapper">
<div class="thumbnail-item active" data-index="0" onclick="gallery.switchToMedia(0)">
<img src="https://via.placeholder.com/80x60/4f46e5/ffffff?text=1" alt="Thumb 1" class="thumbnail-img">
<div class="thumbnail-overlay">
<span class="thumbnail-number">1</span>
</div>
</div>
<div class="thumbnail-item" data-index="1" onclick="gallery.switchToMedia(1)">
<img src="https://via.placeholder.com/80x60/7c3aed/ffffff?text=2" alt="Thumb 2" class="thumbnail-img">
<div class="thumbnail-overlay">
<span class="thumbnail-number">2</span>
</div>
</div>
<div class="thumbnail-item" data-index="2" onclick="gallery.switchToMedia(2)">
<div class="video-thumbnail-placeholder">
<i class="fas fa-play"></i>
</div>
<div class="media-type-badge video">
<i class="fas fa-play"></i>
</div>
<div class="thumbnail-overlay">
<span class="thumbnail-number">3</span>
</div>
</div>
<div class="thumbnail-item" data-index="3" onclick="gallery.switchToMedia(3)">
<div class="embed-thumbnail-placeholder">
<i class="fab fa-youtube"></i>
</div>
<div class="media-type-badge embed">
<i class="fas fa-link"></i>
</div>
<div class="thumbnail-overlay">
<span class="thumbnail-number">4</span>
</div>
</div>
<div class="thumbnail-item" data-index="4" onclick="gallery.switchToMedia(4)">
<img src="https://via.placeholder.com/80x60/06b6d4/ffffff?text=3" alt="Thumb 3" class="thumbnail-img">
<div class="thumbnail-overlay">
<span class="thumbnail-number">5</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Lightbox2 JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js"></script>
<script>
// Класс для управления современной галереей
class ModernMediaGallery {
constructor(container) {
this.container = container;
this.mediaItems = container.querySelectorAll('.main-media-item');
this.thumbnails = container.querySelectorAll('.thumbnail-item');
this.progressBar = container.querySelector('.progress-bar');
this.currentIndex = 0;
this.initGallery();
this.bindEvents();
}
initGallery() {
this.updateProgress();
this.preloadMedia();
}
bindEvents() {
// Клавиатурные события
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
this.previousMedia();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
this.nextMedia();
}
});
// Touch события для свайпа
let startX = 0;
let startY = 0;
this.container.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
this.container.addEventListener('touchend', (e) => {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const deltaX = endX - startX;
const deltaY = endY - startY;
// Проверяем, что это горизонтальный свайп
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
if (deltaX > 0) {
this.previousMedia();
} else {
this.nextMedia();
}
}
}, { passive: true });
}
switchToMedia(index) {
if (index === this.currentIndex) return;
// Останавливаем текущее видео
this.stopCurrentVideo();
// Обновляем активные элементы
this.mediaItems[this.currentIndex].classList.remove('active');
this.thumbnails[this.currentIndex].classList.remove('active');
this.currentIndex = index;
this.mediaItems[this.currentIndex].classList.add('active');
this.thumbnails[this.currentIndex].classList.add('active');
this.updateProgress();
this.scrollToActiveThumbnail();
}
nextMedia() {
const nextIndex = (this.currentIndex + 1) % this.mediaItems.length;
this.switchToMedia(nextIndex);
}
previousMedia() {
const prevIndex = (this.currentIndex - 1 + this.mediaItems.length) % this.mediaItems.length;
this.switchToMedia(prevIndex);
}
updateProgress() {
const progress = ((this.currentIndex + 1) / this.mediaItems.length) * 100;
this.progressBar.style.width = `${progress}%`;
}
scrollToActiveThumbnail() {
const activeThumbnail = this.thumbnails[this.currentIndex];
const container = activeThumbnail.parentElement;
const thumbnailOffsetLeft = activeThumbnail.offsetLeft;
const thumbnailWidth = activeThumbnail.offsetWidth;
const containerWidth = container.offsetWidth;
const scrollLeft = thumbnailOffsetLeft - (containerWidth / 2) + (thumbnailWidth / 2);
container.scrollTo({
left: scrollLeft,
behavior: 'smooth'
});
}
stopCurrentVideo() {
const currentItem = this.mediaItems[this.currentIndex];
const video = currentItem.querySelector('video');
if (video) {
video.pause();
}
}
preloadMedia() {
this.mediaItems.forEach((item, index) => {
if (index <= 2) { // Предзагружаем первые 3 элемента
const img = item.querySelector('img');
if (img && !img.complete) {
// Изображение уже загружается
}
}
});
}
}
// Инициализация галереи
let gallery;
document.addEventListener('DOMContentLoaded', function() {
const galleryContainer = document.querySelector('.modern-media-gallery');
if (galleryContainer) {
gallery = new ModernMediaGallery(galleryContainer);
}
});
// Глобальные функции для интерактивности
function openLightbox(index) {
// Функция для открытия изображения в лайтбоксе
console.log('Открытие лайтбокса для изображения', index);
}
function toggleVideo(index) {
const video = gallery.mediaItems[index].querySelector('video');
if (video.paused) {
video.play();
} else {
video.pause();
}
}
function openFullscreen(index) {
// Функция для открытия встраиваемого контента в полном экране
console.log('Открытие в полном экране', index);
}
</script>
</body>
</html>

View File

@@ -392,7 +392,7 @@
{% endif %} {% endif %}
<div class="p-4"> <div class="p-4">
<h5 class="mb-3">{{ project.name }}</h5> <h5 class="mb-3">{{ project.name }}</h5>
<p class="text-muted mb-3">{{ project.description|truncatewords:15 }}</p> <p class="text-muted mb-3">{{ project.short_description|default:project.description|striptags|truncatewords:15 }}</p>
<a href="{% url 'project_detail' project.pk %}" class="text-primary fw-semibold"> <a href="{% url 'project_detail' project.pk %}" class="text-primary fw-semibold">
Подробнее <i class="fas fa-arrow-right ms-1"></i> Подробнее <i class="fas fa-arrow-right ms-1"></i>
</a> </a>
@@ -455,105 +455,6 @@
</div> </div>
</div> </div>
</section> </section>
<!-- News Section -->
<section class="section-padding bg-light" id="news">
<div class="container-modern">
<div class="text-center mb-5">
<h2 class="display-6 fw-bold mb-3">
Последние <span class="text-gradient">новости</span>
</h2>
</div>
<div class="row g-4">
<div class="col-lg-12">
<div class="news-card bg-white rounded-4 p-4 shadow">
<div class="d-flex align-items-center mb-3">
<span class="badge bg-primary rounded-pill px-3 py-1 me-3">24.11.2025</span>
<h5 class="mb-0">Новый сайт</h5>
</div>
<p class="text-muted mb-3">
Поздравляем всех наших клиентов с этой знаменательной датой!
Мы переписали свой сайт! теперь у нас современный дизайн и улучшенная функциональность...
</p>
<a href="{% url 'news' %}" class="text-primary fw-semibold">
Узнать больше <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="{% url 'news' %}" class="btn btn-primary-modern btn-lg">
<i class="fas fa-newspaper me-2"></i>
Все новости
</a>
</div>
</div>
</section>
<!-- Career Section -->
<section class="section-padding" id="career">
<div class="container-modern">
<div class="text-center mb-5">
<h2 class="display-6 fw-bold mb-3">
Присоединяйтесь к нашей <span class="text-gradient">команде</span>
</h2>
<p class="lead text-muted max-width-600 mx-auto">
Мы ищем талантливых специалистов, которые разделяют нашу страсть к технологиям и инновациям.
</p>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-4">
<div class="career-feature text-center p-4">
<div class="career-icon bg-primary rounded-3 p-3 mx-auto mb-3 text-white" style="width: fit-content;">
<i class="fas fa-chart-line fa-2x"></i>
</div>
<h6 class="mb-2">Профессиональный рост</h6>
<p class="text-muted small mb-0">Возможности для развития и обучения</p>
</div>
</div>
<div class="col-lg-4">
<div class="career-feature text-center p-4">
<div class="career-icon bg-success rounded-3 p-3 mx-auto mb-3 text-white" style="width: fit-content;">
<i class="fas fa-users fa-2x"></i>
</div>
<h6 class="mb-2">Команда профессионалов</h6>
<p class="text-muted small mb-0">Работайте с лучшими специалистами</p>
</div>
</div>
<div class="col-lg-4">
<div class="career-feature text-center p-4">
<div class="career-icon bg-warning rounded-3 p-3 mx-auto mb-3 text-white" style="width: fit-content;">
<i class="fas fa-clock fa-2x"></i>
</div>
<h6 class="mb-2">Гибкий график</h6>
<p class="text-muted small mb-0">Удаленная работа и гибкое расписание</p>
</div>
</div>
</div>
<div class="text-center">
<div class="career-stats-home bg-white rounded-4 p-4 shadow-lg mb-4" style="max-width: 320px; margin: 0 auto; border: 2px solid #667eea;">
<h3 class="display-4 fw-bold mb-2 text-primary">{{ total_open_positions|default:0 }}</h3>
<h6 class="mb-2 text-dark fw-semibold">Открыто вакансий</h6>
<p class="small mb-0 text-muted">Найдите свою идеальную позицию</p>
</div>
<a href="{% url 'career' %}" class="btn btn-primary-modern btn-lg me-3">
<i class="fas fa-briefcase me-2"></i>
Смотреть вакансии
</a>
<a href="{% url 'career' %}" class="btn btn-outline-primary btn-lg">
Посмотреть все
</a>
</div>
</div>
</section>
{% endblock %} {% endblock %}
{% block extra_styles %} {% block extra_styles %}
@@ -901,60 +802,64 @@ document.addEventListener('DOMContentLoaded', function() {
// Показываем текст активного индикатора // Показываем текст активного индикатора
const title = indicator.querySelector('.pill-indicator-title'); const title = indicator.querySelector('.pill-indicator-title');
if (title) { if (title) {
indicator.style.color = '#333'; // Убираем inline стили, чтобы CSS правила работали корректно
title.style.opacity = '1'; indicator.style.color = '';
title.style.transform = 'scale(1)'; title.style.opacity = '';
title.style.transform = '';
} }
} else { } else {
indicator.classList.remove('active'); indicator.classList.remove('active');
// Скрываем текст неактивных индикаторов // Скрываем текст неактивных индикаторов и убираем inline стили
const title = indicator.querySelector('.pill-indicator-title'); const title = indicator.querySelector('.pill-indicator-title');
if (title) { if (title) {
indicator.style.color = 'transparent'; indicator.style.color = '';
title.style.opacity = '0'; title.style.opacity = '';
title.style.transform = 'scale(0.8)'; title.style.transform = '';
// Убираем любые hover эффекты
indicator.style.transform = '';
indicator.style.background = '';
} }
} }
}); });
// Обновляем ширину внешней пилюли // Динамический расчет ширины внешнего контейнера
setTimeout(() => { setTimeout(() => {
if (outerPill) { if (outerPill && indicators.length > 0) {
const activeIndicator = indicators[index]; let totalWidth = 0;
if (activeIndicator) { // Проходим по всем маркерам и суммируем их ширины
// Даем время для применения стилей активного элемента indicators.forEach((indicator, i) => {
setTimeout(() => { if (i === index && indicator.classList.contains('active')) {
// Вычисляем ширину активной пилюли на основе текста // Активный маркер - измеряем его реальную ширину
const titleElement = activeIndicator.querySelector('.pill-indicator-title'); const rect = indicator.getBoundingClientRect();
let activePillWidth = 80; // минимальная ширина активного элемента totalWidth += rect.width || 60; // fallback к минимальной ширине
} else {
// Неактивный маркер - фиксированная ширина 36px
totalWidth += 36;
}
if (titleElement && titleElement.textContent) { // Добавляем gap между маркерами (16px), кроме последнего
// Формула: длина текста * 8px + padding (32px) + min-width if (i < indicators.length - 1) {
const textLength = titleElement.textContent.length; totalWidth += 16;
activePillWidth = Math.max(textLength * 8 + 32, 80); }
} });
// Количество неактивных маркеров // Добавляем padding контейнера: 10px слева + 10px справа
const inactiveCount = indicators.length - 1; totalWidth += 20;
// Ширина неактивных элементов: 32px каждый + margin 4px между ними // Применяем новую ширину
const inactiveWidth = inactiveCount * 32 + (inactiveCount > 0 ? inactiveCount * 4 : 0); outerPill.style.width = totalWidth + 'px';
// Margin активного элемента: 16px (8px с каждой стороны) console.log('Pill state update:');
const activeMargin = inactiveCount > 0 ? 16 : 0; console.log('- Active index:', index);
console.log('- Total width:', totalWidth + 'px');
// Общая ширина: активный + неактивные + margin + padding контейнера console.log('- Active element width:',
const totalWidth = activePillWidth + inactiveWidth + activeMargin + 32; indicators[index] ? indicators[index].getBoundingClientRect().width + 'px' : 'N/A');
console.log('- Inactive elements count:', indicators.length - 1);
console.log('Active pill width:', activePillWidth, 'Inactive width:', inactiveWidth, 'Total:', totalWidth); console.log('- Gaps total:', (indicators.length - 1) * 16 + 'px');
console.log('- Padding total: 20px');
outerPill.style.width = totalWidth + 'px';
outerPill.style.transition = 'all 0.4s cubic-bezier(0.23, 1, 0.32, 1)';
}, 50);
}
} }
}, 10); }, 50);
currentActiveIndex = index; currentActiveIndex = index;
} }
@@ -981,19 +886,23 @@ document.addEventListener('DOMContentLoaded', function() {
if (!this.classList.contains('active')) { if (!this.classList.contains('active')) {
this.style.transform = 'scale(1.1)'; this.style.transform = 'scale(1.1)';
this.style.background = 'rgba(255, 255, 255, 0.6)'; this.style.background = 'rgba(255, 255, 255, 0.6)';
this.style.borderColor = 'rgba(255, 255, 255, 0.7)';
} }
}); });
indicator.addEventListener('mouseleave', function() { indicator.addEventListener('mouseleave', function() {
if (!this.classList.contains('active')) { if (!this.classList.contains('active')) {
this.style.transform = 'scale(1)'; // Возвращаем к исходному состоянию
this.style.background = 'rgba(255, 255, 255, 0.4)'; this.style.transform = '';
this.style.background = '';
this.style.borderColor = '';
} }
}); });
}); });
// Bootstrap carousel события // Bootstrap carousel события
if (carousel) { if (carousel) {
// Обработка начала смены слайда
carousel.addEventListener('slide.bs.carousel', function(event) { carousel.addEventListener('slide.bs.carousel', function(event) {
const nextIndex = event.to; const nextIndex = event.to;
console.log('Carousel sliding to:', nextIndex); console.log('Carousel sliding to:', nextIndex);
@@ -1007,25 +916,74 @@ document.addEventListener('DOMContentLoaded', function() {
}, 400); }, 400);
} }
// Обновляем состояние пилюли сразу при начале смены слайда
updatePillState(nextIndex); updatePillState(nextIndex);
}); });
// Инициализируем первое состояние // Дополнительная обработка завершения смены слайда для надежности
carousel.addEventListener('slid.bs.carousel', function(event) {
const currentIndex = event.to;
console.log('Carousel slide completed:', currentIndex);
// Дополнительное обновление состояния для гарантии корректного отображения
setTimeout(() => {
updatePillState(currentIndex);
}, 100);
});
// Инициализируем первое состояние и рассчитываем начальную ширину
setTimeout(() => { setTimeout(() => {
console.log('Initializing pill state...'); console.log('Initializing pill state...');
updatePillState(0); updatePillState(0);
// Дополнительная инициализация ширины контейнера
if (outerPill && indicators.length > 0) {
let initialWidth = 20; // padding
// Первый элемент активный, остальные неактивные
indicators.forEach((indicator, i) => {
if (i === 0) {
// Даем время активному элементу развернуться
setTimeout(() => {
const rect = indicator.getBoundingClientRect();
let width = 20 + rect.width; // padding + активный элемент
// Добавляем неактивные элементы
if (indicators.length > 1) {
width += (indicators.length - 1) * 36; // неактивные элементы
width += (indicators.length - 1) * 16; // gaps между элементами
}
outerPill.style.width = width + 'px';
console.log('Initial container width:', width + 'px');
}, 100);
}
});
}
}, 200); }, 200);
} }
// Отслеживаем изменения размеров активного элемента // Отслеживаем изменения размеров активного элемента для пересчета ширины
if (window.ResizeObserver) { if (window.ResizeObserver && outerPill) {
const resizeObserver = new ResizeObserver(entries => { const resizeObserver = new ResizeObserver(entries => {
if (outerPill) { // Пересчитываем ширину контейнера при изменении размеров
const activeIndicator = indicators[currentActiveIndex]; let totalWidth = 0;
if (activeIndicator && activeIndicator.classList.contains('active')) {
updatePillState(currentActiveIndex); indicators.forEach((indicator, i) => {
if (indicator.classList.contains('active')) {
const rect = indicator.getBoundingClientRect();
totalWidth += rect.width;
} else {
totalWidth += 36;
} }
}
if (i < indicators.length - 1) {
totalWidth += 16;
}
});
totalWidth += 20; // padding
outerPill.style.width = totalWidth + 'px';
}); });
indicators.forEach(indicator => { indicators.forEach(indicator => {
@@ -1112,66 +1070,6 @@ document.addEventListener('DOMContentLoaded', function() {
display: inline-block; display: inline-block;
margin: 0.5rem; margin: 0.5rem;
} }
.career-stats-home {
max-width: 280px !important;
padding: 1.5rem !important;
}
.career-stats-home h3 {
font-size: 2.5rem !important;
}
}
/* Career Stats Home Styles */
.career-stats-home {
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
border: 2px solid #667eea !important;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.15) !important;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.career-stats-home::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
z-index: 1;
}
.career-stats-home:hover {
transform: translateY(-5px);
box-shadow: 0 20px 50px rgba(102, 126, 234, 0.25) !important;
border-color: #5a67d8;
}
.career-stats-home > * {
position: relative;
z-index: 2;
}
.career-stats-home h3 {
color: #667eea !important;
text-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
font-size: 3rem;
font-weight: 800;
}
.career-stats-home h6 {
color: #2d3748 !important;
font-weight: 600;
font-size: 1.1rem;
letter-spacing: 0.5px;
}
.career-stats-home p {
color: #6b7280 !important;
font-size: 0.9rem;
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -14,28 +14,45 @@
.category-filter { .category-filter {
display: flex; display: flex;
gap: 1rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
margin: 2rem 0; margin: 2rem 0;
justify-content: center; justify-content: center;
padding: 0 1rem;
} }
.category-btn { .category-pill {
padding: 0.75rem 1.5rem; padding: 0.7rem 1.4rem;
border: 2px solid #667eea; background: rgba(255, 255, 255, 0.9);
background: white; backdrop-filter: blur(10px);
color: #667eea; -webkit-backdrop-filter: blur(10px);
color: #495057;
border: 2px solid transparent;
border-radius: 50px; border-radius: 50px;
transition: all 0.3s ease; transition: all 0.3s ease;
font-weight: 600; font-weight: 500;
text-decoration: none;
font-size: 0.9rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
} }
.category-btn:hover, .category-pill:hover {
.category-btn.active { background: rgba(102, 126, 234, 0.1);
color: #667eea;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.3);
text-decoration: none;
}
.category-pill.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
border-color: transparent;
} }
.portfolio-card { .portfolio-card {
@@ -107,6 +124,40 @@
font-weight: 600; font-weight: 600;
z-index: 10; z-index: 10;
} }
/* Адаптивность для мобильных устройств */
@media (max-width: 768px) {
.portfolio-hero {
padding: 80px 0 40px;
}
.portfolio-hero h1 {
font-size: 2rem;
}
.portfolio-hero .lead {
font-size: 1rem;
}
.category-filter {
gap: 0.5rem;
padding: 0 0.5rem;
}
.category-pill {
font-size: 0.85rem;
padding: 0.6rem 1rem;
}
.portfolio-card {
margin-bottom: 1.5rem;
}
.featured-badge {
font-size: 0.8rem;
padding: 0.4rem 0.8rem;
}
}
</style> </style>
{% endblock %} {% endblock %}
@@ -120,11 +171,11 @@
<div class="container py-5"> <div class="container py-5">
<div class="category-filter"> <div class="category-filter">
<a href="{% url 'portfolio_list' %}" class="category-btn {% if not request.GET.category %}active{% endif %}"> <a href="{% url 'portfolio_list' %}" class="category-pill {% if not request.GET.category %}active{% endif %}">
<i class="fas fa-th me-2"></i>Все проекты <i class="fas fa-th me-2"></i>Все проекты
</a> </a>
{% for category in categories %} {% for category in categories %}
<a href="?category={{ category.slug }}" class="category-btn {% if request.GET.category == category.slug %}active{% endif %}"> <a href="?category={{ category.slug }}" class="category-pill {% if request.GET.category == category.slug %}active{% endif %}">
<i class="{{ category.icon }} me-2"></i>{{ category.name }} <i class="{{ category.icon }} me-2"></i>{{ category.name }}
</a> </a>
{% endfor %} {% endfor %}

File diff suppressed because it is too large Load Diff

View File

@@ -13,124 +13,195 @@
margin-bottom: 3rem; margin-bottom: 3rem;
} }
/* Category Filter - Овальные пилюли */
.category-filter { .category-filter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin-bottom: 3rem; margin-bottom: 3rem;
text-align: center; padding: 0 1rem;
} }
.category-btn { .category-pill {
display: inline-block; display: inline-flex;
padding: 0.75rem 1.5rem; align-items: center;
margin: 0.5rem 0.25rem; padding: 0.75rem 1.25rem;
border-radius: 25px;
background: white; background: white;
border: 2px solid #e2e8f0; border: 2px solid #e2e8f0;
color: #4a5568; border-radius: 50px; /* Овальная форма */
color: #64748b;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease;
font-weight: 500; font-weight: 500;
font-size: 0.9rem;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
.category-btn:hover { .category-pill:hover {
border-color: #667eea;
color: #667eea; color: #667eea;
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
text-decoration: none;
} }
.category-btn.active { .category-pill.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: transparent; border-color: transparent;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
transform: translateY(-1px);
}
.category-pill.active:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6b4190 100%);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
color: white;
}
.category-pill i {
font-size: 0.85rem;
margin-right: 0.5rem;
} }
.projects-grid { .projects-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
max-width: 100%;
gap: 2rem; gap: 2rem;
margin-bottom: 3rem; margin-bottom: 3rem;
} }
@media (max-width: 992px) { /* Ограничиваем максимальный размер карточки */
.projects-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
@media (max-width: 576px) {
.projects-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
.project-card { .project-card {
max-width: 400px;
width: 100%;
background: white; background: white;
border-radius: 20px; border-radius: 20px;
overflow: hidden; overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.1); box-shadow: 0 8px 25px rgba(0,0,0,0.08);
transition: box-shadow 0.3s ease !important; transition: all 0.3s ease;
margin-bottom: 0; margin-bottom: 0;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #f1f5f9;
} }
.project-card:hover { .project-card:hover {
box-shadow: 0 15px 40px rgba(0,0,0,0.2) !important; box-shadow: 0 20px 50px rgba(0,0,0,0.15);
transform: none !important; transform: translateY(-5px);
border-color: #e2e8f0;
} }
.project-thumbnail { /* Верхняя часть карточки - медиа контент */
.project-media {
width: 100%; width: 100%;
height: 200px; height: 180px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
} }
.project-thumbnail img { .project-media img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
} }
.project-card:hover .project-thumbnail img { .project-card:hover .project-media img {
transform: scale(1.15) translateY(-10px) !important; transform: scale(1.08);
} }
.project-thumbnail::after { .project-media video {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
/* Заглушка для проектов без изображения */
.project-media.no-image {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.project-media.no-image::before {
content: '\f1c2'; /* FontAwesome folder icon */
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 2.5rem;
opacity: 0.7;
}
.project-media::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 50%; height: 40%;
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent); background: linear-gradient(to top, rgba(0,0,0,0.4), transparent);
pointer-events: none; pointer-events: none;
} }
/* Медиа плеер для видео */
.media-player {
position: relative;
width: 100%;
height: 100%;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 50%;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #667eea;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.play-btn:hover {
background: white;
transform: translate(-50%, -50%) scale(1.1);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.project-badge { .project-badge {
position: absolute; position: absolute;
top: 10px; top: 15px;
right: 10px; right: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
padding: 0.35rem 0.75rem; padding: 0.4rem 0.8rem;
border-radius: 15px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
z-index: 1; z-index: 2;
box-shadow: 0 4px 15px rgba(0,0,0,0.2); box-shadow: 0 4px 15px rgba(0,0,0,0.25);
border: 1px solid rgba(255, 255, 255, 0.2);
} }
/* Основной контент карточки */
.project-content { .project-content {
padding: 1rem; padding: 1rem;
flex-grow: 1; flex-grow: 1;
@@ -141,66 +212,135 @@
.project-title { .project-title {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
color: #2d3748; color: #1a202c;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
line-height: 1.3; line-height: 1.3;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
min-height: 2.6rem; /* Фиксированная высота для выравнивания */
} }
.project-description { .project-description {
color: #718096; color: #64748b;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
flex-grow: 1; flex-grow: 1;
line-height: 1.5; line-height: 1.5;
font-size: 0.9rem; font-size: 0.85rem;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
min-height: 2.5rem; /* Фиксированная высота для выравнивания */
}
/* Статистика проекта */
.project-stats-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding: 0.5rem 0.75rem;
background: #f8fafc;
border-radius: 8px;
font-size: 0.8rem;
}
.stats-left {
display: flex;
gap: 1rem;
}
.stat-item {
display: flex;
align-items: center;
gap: 0.35rem;
color: #64748b;
}
.stat-item i {
color: #667eea;
font-size: 0.85rem;
}
.project-year {
color: #94a3b8;
font-weight: 500;
}
/* Футер карточки - категории и дополнительная информация */
.project-footer {
padding: 0 1rem 1rem;
border-top: 1px solid #f1f5f9;
margin-top: auto;
}
.project-categories {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.category-tag {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
color: #667eea;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 500;
white-space: nowrap;
border: 1px solid rgba(102, 126, 234, 0.2);
}
.category-tag i {
margin-right: 0.25rem;
font-size: 0.75rem;
} }
.project-meta { .project-meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-top: 0.75rem;
border-top: 1px solid #e2e8f0;
margin-top: auto;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: #94a3b8;
} }
.project-stats { .project-info {
display: flex; display: flex;
gap: 0.75rem; gap: 1rem;
font-size: 0.85rem; font-size: 0.8rem;
color: #718096;
} }
.project-stats i { .project-info span {
color: #667eea;
}
.project-categories {
margin-bottom: 0.75rem;
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
gap: 0.35rem; gap: 0.25rem;
} }
.category-tag { .project-status {
display: inline-block; display: inline-flex;
padding: 0.2rem 0.6rem; align-items: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); gap: 0.25rem;
color: #667eea; padding: 0.25rem 0.75rem;
border-radius: 12px; border-radius: 15px;
font-size: 0.75rem; font-size: 0.75rem;
font-size: 0.85rem; font-weight: 600;
white-space: nowrap; }
.status-completed {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.status-progress {
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
border: 1px solid rgba(59, 130, 246, 0.2);
} }
.no-projects { .no-projects {
@@ -229,22 +369,66 @@
font-size: 1rem; font-size: 1rem;
} }
.project-thumbnail { .category-filter {
height: 180px; gap: 0.5rem;
padding: 0 0.5rem;
}
.category-pill {
font-size: 0.85rem;
padding: 0.6rem 1rem;
}
.projects-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.project-card {
max-width: 350px;
}
.project-media {
height: 160px;
} }
.project-title { .project-title {
font-size: 1rem; font-size: 1rem;
min-height: 2.2rem;
}
.project-content {
padding: 0.85rem;
}
.project-footer {
padding: 0 0.85rem 0.85rem;
}
.stats-left {
gap: 0.75rem;
} }
.project-meta { .project-meta {
font-size: 0.8rem; font-size: 0.75rem;
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
} }
} }
@media (max-width: 576px) { @media (max-width: 576px) {
.project-thumbnail { .projects-grid {
height: 160px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.project-card {
max-width: 320px;
}
.project-media {
height: 140px;
} }
.category-btn { .category-btn {
@@ -255,6 +439,42 @@
.project-content { .project-content {
padding: 0.75rem; padding: 0.75rem;
} }
.project-footer {
padding: 0 0.75rem 0.75rem;
}
.project-title {
font-size: 0.95rem;
min-height: 2rem;
}
.project-description {
min-height: 2rem;
font-size: 0.8rem;
}
.project-stats-section {
padding: 0.4rem 0.6rem;
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
font-size: 0.75rem;
}
.stats-left {
gap: 1rem;
}
.project-info {
gap: 0.75rem;
font-size: 0.7rem;
}
.category-tag {
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
}
} }
/* Скелетон загрузка */ /* Скелетон загрузка */
@@ -284,12 +504,12 @@
<div class="container"> <div class="container">
{% if categories %} {% if categories %}
<div class="category-filter"> <div class="category-filter">
<a href="{% url 'projects_list' %}" class="category-btn {% if not selected_category %}active{% endif %}"> <a href="{% url 'projects_list' %}" class="category-pill {% if not selected_category %}active{% endif %}">
<i class="fas fa-th me-2"></i>Все проекты <i class="fas fa-th me-2"></i>Все проекты
</a> </a>
{% for category in categories %} {% for category in categories %}
<a href="{% url 'projects_list' %}?category={{ category.id }}" <a href="{% url 'projects_list' %}?category={{ category.id }}"
class="category-btn {% if selected_category == category.id|stringformat:'s' %}active{% endif %}"> class="category-pill {% if selected_category == category.id|stringformat:'s' %}active{% endif %}">
<i class="{{ category.icon }} me-2"></i>{{ category.name }} <i class="{{ category.icon }} me-2"></i>{{ category.name }}
</a> </a>
{% endfor %} {% endfor %}
@@ -301,51 +521,91 @@
{% for project in projects %} {% for project in projects %}
<a href="{% url 'project_detail' project.pk %}" class="text-decoration-none"> <a href="{% url 'project_detail' project.pk %}" class="text-decoration-none">
<div class="project-card"> <div class="project-card">
<div class="project-thumbnail"> <!-- Верхняя часть - медиа контент -->
{% if project.thumbnail %} <div class="project-media{% if not project.video and not project.thumbnail and not project.image %} no-image{% endif %}">
<img src="{{ project.thumbnail.url }}" alt="{{ project.name }}" loading="lazy" decoding="async"> {% if project.video %}
{% elif project.image %} <div class="media-player">
<img src="{{ project.image.url }}" alt="{{ project.name }}" loading="lazy" decoding="async"> <video poster="{% if project.video_poster %}{{ project.video_poster.url }}{% elif project.thumbnail %}{{ project.thumbnail.url }}{% endif %}"
{% else %} preload="metadata" muted>
<img src="{% static 'img/default-project.jpg' %}" alt="{{ project.name }}" loading="lazy" decoding="async"> <source src="{{ project.video.url }}" type="video/mp4">
{% endif %} </video>
<button class="play-btn" type="button" aria-label="Воспроизвести видео">
{% if project.is_featured %} <i class="fas fa-play"></i>
<div class="project-badge"> </button>
<i class="fas fa-star me-1"></i>Избранное
</div> </div>
{% elif project.thumbnail %}
<img src="{{ project.thumbnail.url }}" alt="{{ project.name }}" loading="lazy" decoding="async">
{% elif project.image %}
<img src="{{ project.image.url }}" alt="{{ project.name }}" loading="lazy" decoding="async">
{% endif %}
{% if project.is_featured %}
<div class="project-badge">
<i class="fas fa-star me-1"></i>Избранное
</div>
{% endif %}
</div>
<!-- Основной контент карточки -->
<div class="project-content">
<h3 class="project-title">{{ project.name }}</h3>
{% if project.short_description %}
<p class="project-description">{{ project.short_description|striptags|truncatewords:25 }}</p>
{% endif %}
<!-- Статистика проекта -->
<div class="project-stats-section">
<div class="stats-left">
<div class="stat-item">
<i class="fas fa-eye"></i>
<span>{{ project.views_count }}</span>
</div>
<div class="stat-item">
<i class="fas fa-heart"></i>
<span>{{ project.likes_count }}</span>
</div>
</div>
{% if project.completion_date %}
<div class="project-year">{{ project.completion_date|date:"Y" }}</div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="project-content"> <!-- Футер карточки -->
<h3 class="project-title">{{ project.name }}</h3> <div class="project-footer">
{% if project.categories.exists %}
<div class="project-categories">
{% for category in project.categories.all %}
<span class="category-tag">
<i class="{{ category.icon }}"></i>{{ category.name }}
</span>
{% endfor %}
</div>
{% endif %}
{% if project.categories.exists %} <div class="project-meta">
<div class="project-categories"> <div class="project-info">
{% for category in project.categories.all %} {% if project.duration %}
<span class="category-tag"> <span><i class="fas fa-clock"></i>{{ project.duration }}</span>
<i class="{{ category.icon }} me-1"></i>{{ category.name }} {% endif %}
</span> {% if project.team_size %}
{% endfor %} <span><i class="fas fa-users"></i>{{ project.team_size }}</span>
{% endif %}
</div> </div>
{% endif %} <div class="project-status status-{{ project.status }}">
{% if project.status == 'completed' %}
{% if project.short_description %} <i class="fas fa-check-circle"></i>Завершен
<p class="project-description">{{ project.short_description|truncatewords:20 }}</p> {% elif project.status == 'in_progress' %}
{% endif %} <i class="fas fa-spinner"></i>В процессе
{% else %}
<div class="project-meta"> <i class="fas fa-archive"></i>В архиве
<div class="project-stats">
<span><i class="fas fa-eye me-1"></i>{{ project.views_count }}</span>
<span><i class="fas fa-heart me-1"></i>{{ project.likes_count }}</span>
</div>
{% if project.completion_date %}
<small class="text-muted">{{ project.completion_date|date:"Y" }}</small>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</a> </div>
</a>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
@@ -360,3 +620,134 @@
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Обработчик для видео плеера
const playBtns = document.querySelectorAll('.play-btn');
playBtns.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const mediaPlayer = this.closest('.media-player');
const video = mediaPlayer.querySelector('video');
if (video) {
if (video.paused) {
video.play();
this.style.opacity = '0';
} else {
video.pause();
this.style.opacity = '1';
}
}
});
});
// Обработчики событий видео
const videos = document.querySelectorAll('.media-player video');
videos.forEach(video => {
const playBtn = video.closest('.media-player').querySelector('.play-btn');
video.addEventListener('play', () => {
if (playBtn) playBtn.style.opacity = '0';
});
video.addEventListener('pause', () => {
if (playBtn) playBtn.style.opacity = '1';
});
video.addEventListener('ended', () => {
if (playBtn) playBtn.style.opacity = '1';
});
});
// Анимация появления карточек при загрузке
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Применяем анимацию к карточкам
const cards = document.querySelectorAll('.project-card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(30px)';
card.style.transition = `opacity 0.6s ease ${index * 0.1}s, transform 0.6s ease ${index * 0.1}s`;
observer.observe(card);
});
// Lazy loading для изображений
const images = document.querySelectorAll('img[loading="lazy"]');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => {
imageObserver.observe(img);
});
}
});
</script>
<style>
/* Анимации для загруженных изображений */
.project-media img {
transition: opacity 0.3s ease;
opacity: 0.8;
}
.project-media img.loaded {
opacity: 1;
}
/* Плавная анимация при hover */
.project-card {
will-change: transform, box-shadow;
}
.project-card:hover .project-media img {
will-change: transform;
}
/* Улучшенные переходы для статуса */
.status-completed {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.status-in_progress {
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.status-archived {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
border: 1px solid rgba(107, 114, 128, 0.2);
}
</style>
{% endblock %}

View File

@@ -47,7 +47,7 @@
<span class="badge bg-primary-modern">{{ service.category.name }}</span> <span class="badge bg-primary-modern">{{ service.category.name }}</span>
</div> </div>
<h1 class="display-5 fw-bold mb-4">{{ service.name }}</h1> <h1 class="display-5 fw-bold mb-4">{{ service.name }}</h1>
<p class="lead text-muted mb-4">{{ service.description }}</p> <p class="lead text-muted mb-4">{{ service.description|striptags }}</p>
<!-- Service Features --> <!-- Service Features -->
<div class="mb-4"> <div class="mb-4">
@@ -252,7 +252,7 @@
<h5 class="mb-3 text-dark">{{ project.name }}</h5> <h5 class="mb-3 text-dark">{{ project.name }}</h5>
{% if project.short_description %} {% if project.short_description %}
<p class="text-muted mb-3">{{ project.short_description|truncatewords:15 }}</p> <p class="text-muted mb-3">{{ project.short_description|striptags|truncatewords:15 }}</p>
{% else %} {% else %}
<p class="text-muted mb-3">{{ project.description|striptags|truncatewords:15 }}</p> <p class="text-muted mb-3">{{ project.description|striptags|truncatewords:15 }}</p>
{% endif %} {% endif %}
@@ -417,21 +417,16 @@ document.addEventListener('DOMContentLoaded', function() {
// Get CSRF token // Get CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
// Prepare data for submission // Prepare data for QR code generation
const serviceId = document.getElementById('serviceId').value; const serviceId = document.getElementById('serviceId').value;
const clientData = { const clientData = {
service_id: serviceId, client_name: formData.get('name'),
first_name: formData.get('first_name'), client_email: formData.get('email'),
last_name: formData.get('last_name'), client_phone: formData.get('phone')
email: formData.get('email'),
phone: formData.get('phone'),
description: formData.get('description'),
budget: formData.get('budget'),
timeline: formData.get('timeline')
}; };
// Submit to server // Submit to QR code generation endpoint
fetch(`/service/request/${serviceId}/`, { fetch(`/service/generate_qr_code/${serviceId}/`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -441,24 +436,41 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.registration_link) {
// Show success animation // Hide form and show QR code
document.querySelector('.modal-body form').style.display = 'none'; document.querySelector('.modal-body form').style.display = 'none';
document.getElementById('successSection').style.display = 'block'; document.getElementById('successSection').style.display = 'none';
// Close modal after delay // Create QR code section
setTimeout(() => { const qrSection = document.createElement('div');
const modal = bootstrap.Modal.getInstance(document.getElementById('serviceModal')); qrSection.id = 'qrSection';
if (modal) { qrSection.className = 'text-center py-4';
modal.hide(); qrSection.innerHTML = `
} <h4 class="text-primary mb-3">Завершите заявку в Telegram</h4>
// Reset form <p class="text-muted mb-4">Отсканируйте QR-код или перейдите по ссылке для завершения регистрации заявки</p>
form.reset(); <div class="qr-code-container mb-4">
document.querySelector('.modal-body form').style.display = 'block'; <img src="${data.qr_code_url}" alt="QR Code" class="img-fluid" style="max-width: 250px; border: 2px solid #dee2e6; border-radius: 8px;">
document.getElementById('successSection').style.display = 'none'; </div>
}, 3000); <div class="d-grid gap-2">
<a href="${data.registration_link}" target="_blank" class="btn btn-primary btn-lg">
<i class="fab fa-telegram-plane me-2"></i>
Открыть в Telegram
</a>
<button type="button" class="btn btn-outline-secondary" onclick="resetModal()" data-bs-dismiss="modal">
Закрыть
</button>
</div>
`;
document.querySelector('.modal-body').appendChild(qrSection);
// Hide footer buttons
document.querySelector('.modal-footer').style.display = 'none';
} else if (data.status === 'existing_request') {
alert('У вас уже есть активная заявка на данную услугу. Проверьте ваш Telegram.');
} else { } else {
alert('Ошибка при отправке заявки: ' + (data.message || 'Попробуйте позже')); alert('Ошибка при создании заявки: ' + (data.error || 'Попробуйте позже'));
} }
}) })
.catch(error => { .catch(error => {
@@ -473,6 +485,33 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
}); });
// Function to reset modal to initial state
function resetModal() {
const modal = document.getElementById('serviceModal');
const form = document.getElementById('serviceRequestForm');
const qrSection = document.getElementById('qrSection');
// Remove QR section if exists
if (qrSection) {
qrSection.remove();
}
// Show form and hide success section
document.querySelector('.modal-body form').style.display = 'block';
document.getElementById('successSection').style.display = 'none';
// Show footer
document.querySelector('.modal-footer').style.display = 'flex';
// Reset form
form.reset();
}
// Reset modal when it's closed
document.getElementById('serviceModal').addEventListener('hidden.bs.modal', function () {
resetModal();
});
</script> </script>
<style> <style>
@@ -689,5 +728,102 @@ document.addEventListener('DOMContentLoaded', function() {
font-size: 1rem; font-size: 1rem;
} }
} }
/* Исправление кликабельности кнопок */
.service-info {
position: relative;
z-index: 100;
}
.service-info .btn,
.service-info .d-flex {
position: relative;
z-index: 101;
pointer-events: auto;
}
.btn:hover {
pointer-events: auto !important;
cursor: pointer !important;
}
/* Убираем возможные перекрывающие псевдоэлементы */
*::before,
*::after {
pointer-events: none;
}
/* Обеспечиваем кликабельность всех интерактивных элементов */
button,
a,
input,
select,
textarea {
pointer-events: auto !important;
position: relative;
z-index: 10;
}
</style> </style>
<!-- Service Request Modal -->
<div class="modal fade" id="serviceModal" tabindex="-1" aria-labelledby="serviceModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="serviceModalLabel">Заказать услугу</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="serviceRequestForm">
{% csrf_token %}
<input type="hidden" id="serviceId" name="service_id">
<div class="row g-3">
<div class="col-md-6">
<label for="clientName" class="form-label">Имя *</label>
<input type="text" class="form-control" id="clientName" name="name" required>
</div>
<div class="col-md-6">
<label for="clientEmail" class="form-label">Email *</label>
<input type="email" class="form-control" id="clientEmail" name="email" required>
</div>
<div class="col-md-6">
<label for="clientPhone" class="form-label">Телефон *</label>
<input type="tel" class="form-control" id="clientPhone" name="phone" required>
</div>
<div class="col-md-6">
<label for="clientCompany" class="form-label">Компания</label>
<input type="text" class="form-control" id="clientCompany" name="company">
</div>
<div class="col-12">
<label for="projectDescription" class="form-label">Описание проекта</label>
<textarea class="form-control" id="projectDescription" name="description" rows="4"
placeholder="Расскажите подробнее о вашем проекте..."></textarea>
</div>
</div>
</form>
<!-- Success Section -->
<div id="successSection" style="display: none;" class="text-center py-4">
<div class="success-checkmark">
<div class="check-icon">
<span class="icon-line line-tip"></span>
<span class="icon-line line-long"></span>
<div class="icon-circle"></div>
<div class="icon-fix"></div>
</div>
</div>
<h4 class="text-success mt-3">Заявка отправлена!</h4>
<p class="text-muted">Мы свяжемся с вами в ближайшее время</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" form="serviceRequestForm" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>Отправить заявку
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -152,7 +152,7 @@ def about_view(request):
def blog_view(request): def blog_view(request):
blog_posts = BlogPost.objects.all().order_by('-created_at') blog_posts = BlogPost.objects.all().order_by('-published_date')
return render(request, 'web/blog.html', {'blog_posts': blog_posts}) return render(request, 'web/blog.html', {'blog_posts': blog_posts})
def news_view(request): def news_view(request):
@@ -229,10 +229,18 @@ def create_service_request(request, service_id):
service=service, service=service,
client=client, client=client,
token=token, token=token,
message=full_description,
is_verified=False is_verified=False
) )
# Create associated Order with message
Order.objects.create(
service_request=service_request,
client=client,
service=service,
message=full_description,
status='pending'
)
return JsonResponse({ return JsonResponse({
'success': True, 'success': True,
'message': 'Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.', 'message': 'Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.',
@@ -256,9 +264,9 @@ def generate_qr_code(request, service_id):
client_phone = data.get('client_phone') client_phone = data.get('client_phone')
client_name = data.get('client_name') client_name = data.get('client_name')
if not all([client_email, client_phone, client_name]): if not all([client_email, client_name, client_phone]):
logger.error("Все поля должны быть заполнены") logger.error("Все обязательные поля должны быть заполнены")
return JsonResponse({'error': 'Все поля должны быть заполнены'}, status=400) return JsonResponse({'error': 'Все обязательные поля должны быть заполнены'}, status=400)
# Используем транзакцию для предотвращения конкурентного создания дубликатов # Используем транзакцию для предотвращения конкурентного создания дубликатов
with transaction.atomic(): with transaction.atomic():