Добавлена система проектов с автоматическим ресайзом изображений и адаптивным дизайном
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
- Удалена старая система портфолио (PortfolioCategory, PortfolioItem) - Расширена модель Project: slug, categories (M2M), thumbnail, media files, meta fields - Объединены категории: ProjectCategory удалена, используется общая Category - Автоматический ресайз thumbnail до 600x400px с умным кропом по центру - Создан /projects/ - страница списка проектов с фильтрацией по категориям - Создан /project/<pk>/ - детальная страница проекта с галереей Swiper - Адаптивный дизайн: 3 карточки в ряд (десктоп), 2 (планшет), 1 (мобильный) - Параллакс-эффект на изображениях при наведении - Lazy loading для оптимизации загрузки - Фильтры категорий в виде пилюль как на странице услуг - Компактные карточки с фиксированной шириной - Кликабельные проекты в service_detail с отображением всех медиа
This commit is contained in:
45
create_test_data.py
Normal file
45
create_test_data.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartsoltech.settings')
|
||||
django.setup()
|
||||
|
||||
from web.models import ProjectCategory, Project
|
||||
|
||||
# Создаём категорию
|
||||
cat, created = ProjectCategory.objects.get_or_create(
|
||||
slug='web-development',
|
||||
defaults={
|
||||
'name': 'Веб-разработка',
|
||||
'description': 'Разработка современных веб-приложений',
|
||||
'icon': 'fas fa-laptop-code',
|
||||
'order': 1,
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
print(f"{'Создана' if created else 'Найдена'} категория: {cat.name}")
|
||||
|
||||
# Обновляем первый проект
|
||||
project = Project.objects.first()
|
||||
if project:
|
||||
project.short_description = 'Корпоративный сайт SmartSolTech с современным дизайном'
|
||||
project.description = '<h2>О проекте</h2><p>Разработка корпоративного сайта с использованием Django и современного дизайна.</p><h3>Особенности</h3><ul><li>Адаптивный дизайн</li><li>Админ-панель</li><li>Интеграция с Telegram</li></ul>'
|
||||
if not project.slug:
|
||||
project.slug = 'smartsoltech-website'
|
||||
project.technologies = 'Python, Django, PostgreSQL, Bootstrap, JavaScript'
|
||||
project.duration = '3 месяца'
|
||||
project.team_size = 4
|
||||
project.is_featured = True
|
||||
project.display_order = 1
|
||||
project.save()
|
||||
project.categories.add(cat)
|
||||
print(f"Обновлён проект: {project.name}")
|
||||
print(f"URL: /project/{project.pk}/")
|
||||
else:
|
||||
print("Проектов не найдено")
|
||||
|
||||
print("\n=== Статистика ===")
|
||||
print(f"Категорий: {ProjectCategory.objects.count()}")
|
||||
print(f"Проектов: {Project.objects.count()}")
|
||||
print(f"Завершённых проектов: {Project.objects.filter(status='completed').count()}")
|
||||
@@ -21,4 +21,7 @@ dj-database-url==2.1.0
|
||||
coverage==7.3.2
|
||||
pytest==7.4.3
|
||||
pytest-django==4.7.0
|
||||
pytest-cov==4.1.0
|
||||
pytest-cov==4.1.0
|
||||
django-tinymce==4.1.0
|
||||
Pillow==10.4.0
|
||||
django-tinymce==4.1.0
|
||||
@@ -60,6 +60,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'tinymce',
|
||||
'web',
|
||||
'comunication'
|
||||
]
|
||||
@@ -158,6 +159,36 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Папка для соб
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# TinyMCE Configuration
|
||||
TINYMCE_DEFAULT_CONFIG = {
|
||||
'height': 500,
|
||||
'width': '100%',
|
||||
'cleanup_on_startup': True,
|
||||
'custom_undo_redo_levels': 20,
|
||||
'selector': 'textarea',
|
||||
'theme': 'silver',
|
||||
'plugins': '''
|
||||
textcolor save link image media preview codesample contextmenu
|
||||
table code lists fullscreen insertdatetime nonbreaking
|
||||
contextmenu directionality searchreplace wordcount visualblocks
|
||||
visualchars code fullscreen autolink lists charmap print hr
|
||||
anchor pagebreak
|
||||
''',
|
||||
'toolbar1': '''
|
||||
fullscreen preview bold italic underline | fontselect,
|
||||
fontsizeselect | forecolor backcolor | alignleft alignright |
|
||||
aligncenter alignjustify | indent outdent | bullist numlist table |
|
||||
| link image media | codesample |
|
||||
''',
|
||||
'toolbar2': '''
|
||||
visualblocks visualchars |
|
||||
charmap hr pagebreak nonbreaking anchor | code |
|
||||
''',
|
||||
'contextmenu': 'formats | link image',
|
||||
'menubar': True,
|
||||
'statusbar': True,
|
||||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
# smartsoltech/urls.py
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('tinymce/', include('tinymce.urls')),
|
||||
path('', include('web.urls')), # Включаем маршруты приложения web
|
||||
]
|
||||
|
||||
# Serve media files in development
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
@@ -281,28 +281,6 @@ p {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Dark mode toggle */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: var(--gradient-primary);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
|
||||
@@ -21,46 +21,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('SmartSolTech: Loading screen not found');
|
||||
}
|
||||
|
||||
// Theme Toggle Functionality
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
if (themeToggle) {
|
||||
// Check for saved theme preference
|
||||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||||
html.setAttribute('data-theme', currentTheme);
|
||||
updateThemeIcon(currentTheme);
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// Add animation
|
||||
this.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
if (!themeToggle) return;
|
||||
const icon = themeToggle.querySelector('i');
|
||||
if (icon) {
|
||||
if (theme === 'dark') {
|
||||
icon.className = 'fas fa-sun';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на светлую тему');
|
||||
} else {
|
||||
icon.className = 'fas fa-moon';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на темную тему');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navbar scroll behavior
|
||||
const navbar = document.querySelector('.navbar-modern');
|
||||
if (navbar) {
|
||||
|
||||
@@ -281,28 +281,6 @@ p {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Dark mode toggle */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: var(--gradient-primary);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
@@ -363,6 +341,15 @@ p {
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
#loading-screen {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
#loading-screen.hidden {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -375,4 +362,764 @@ p {
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Success Checkmark Animation */
|
||||
.success-checkmark {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #4CAF50;
|
||||
stroke-miterlimit: 10;
|
||||
margin: 10px auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.success-checkmark.animate .icon-circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #4CAF50;
|
||||
fill: none;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.success-checkmark.animate .icon-line {
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
||||
}
|
||||
|
||||
.success-checkmark.animate .icon-line.line-tip {
|
||||
animation-delay: 1.1s;
|
||||
}
|
||||
|
||||
.success-checkmark.animate .icon-line.line-long {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.success-checkmark .icon-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #4CAF50;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.success-checkmark .icon-line {
|
||||
height: 2px;
|
||||
background-color: #4CAF50;
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.success-checkmark .icon-line.line-tip {
|
||||
top: 46px;
|
||||
left: 14px;
|
||||
width: 25px;
|
||||
transform: rotate(45deg);
|
||||
animation: icon-line-tip 0.75s;
|
||||
}
|
||||
|
||||
.success-checkmark .icon-line.line-long {
|
||||
top: 38px;
|
||||
right: 8px;
|
||||
width: 47px;
|
||||
transform: rotate(-45deg);
|
||||
animation: icon-line-long 0.75s;
|
||||
}
|
||||
|
||||
.success-checkmark .icon-fix {
|
||||
top: 8px;
|
||||
width: 5px;
|
||||
left: 26px;
|
||||
z-index: 1;
|
||||
height: 85px;
|
||||
position: absolute;
|
||||
transform: rotate(-45deg);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-line-tip {
|
||||
0% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
54% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
70% {
|
||||
width: 50px;
|
||||
left: -8px;
|
||||
top: 37px;
|
||||
}
|
||||
84% {
|
||||
width: 17px;
|
||||
left: 21px;
|
||||
top: 48px;
|
||||
}
|
||||
100% {
|
||||
width: 25px;
|
||||
left: 14px;
|
||||
top: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-line-long {
|
||||
0% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
65% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
84% {
|
||||
width: 55px;
|
||||
right: 0px;
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
width: 47px;
|
||||
right: 8px;
|
||||
top: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Video Support Styles */
|
||||
.hero-video {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.service-video {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.service-video:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.3) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Video Loading States */
|
||||
.video-loading {
|
||||
background: linear-gradient(45deg, #f3f4f6, #e5e7eb);
|
||||
background-size: 400% 400%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Video Controls */
|
||||
.video-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.video-play-btn {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-color);
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.video-play-btn:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Responsive Video */
|
||||
@media (max-width: 768px) {
|
||||
.hero-video,
|
||||
.service-video {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero Container Styles */
|
||||
.hero-modern {
|
||||
padding: 0;
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(99, 102, 241, 0.25);
|
||||
position: relative;
|
||||
margin: 1.5rem auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: relative;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.hero-video,
|
||||
.hero-bg img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8) 0%, rgba(139, 92, 246, 0.6) 100%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 4rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Custom Carousel Indicators - Pill System */
|
||||
.carousel-indicators-container {
|
||||
position: absolute;
|
||||
bottom: 2.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.outer-pill {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
min-height: 50px;
|
||||
transition: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Удаляем старую структуру inner-pill */
|
||||
|
||||
.pill-indicators {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill-indicator {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: width 0.8s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
padding 0.8s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
border-color 0.3s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
overflow: visible;
|
||||
transform-origin: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pill-indicator.active {
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
width: fit-content;
|
||||
min-width: 36px;
|
||||
padding: 0 1rem;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.pill-indicator:not(.active):hover {
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pill-indicator::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
transition: opacity 0.4s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
transform-origin: center;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pill-indicator.active::before {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.pill-indicator:hover:not(.active)::before {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.pill-indicator-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transition: opacity 0.6s cubic-bezier(0.23, 1, 0.32, 1) 0.2s,
|
||||
transform 0.6s cubic-bezier(0.23, 1, 0.32, 1) 0.2s;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
transform-origin: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.pill-indicator.active .pill-indicator-title {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Плавная анимация как вода */
|
||||
.pill-indicator:not(.active):hover {
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Новые стили для расширенной главной страницы */
|
||||
.project-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.project-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.project-image img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.project-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.project-card:hover .project-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.project-card:hover .project-image img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.blog-image {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blog-image img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.blog-card:hover .blog-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.news-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.career-feature {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.career-feature:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.career-icon {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.career-feature:hover .career-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.career-stats {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.career-stats:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.hover-lift {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Carousel Controls */
|
||||
.hero-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0.8;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.carousel-control-prev {
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
.carousel-control-next {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.carousel-control-prev:hover,
|
||||
.carousel-control-next:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Hero Text Styles */
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.hero-container {
|
||||
border-radius: 16px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
padding: 2rem 1.5rem;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.carousel-indicators-container {
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.outer-pill {
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.pill-indicators {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pill-indicator.active {
|
||||
padding: 0 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.pill-indicator-title {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.pill-indicator {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.inner-pill {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.inner-pill.active {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.pill-title {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Дополнительные эффекты для пилюль */
|
||||
.inner-pill::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(99, 102, 241, 0.2) 50%,
|
||||
transparent 100%);
|
||||
border-radius: 25px;
|
||||
opacity: 0;
|
||||
animation: pillShimmer 3s infinite;
|
||||
}
|
||||
|
||||
.outer-pill.expanded .inner-pill::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Эффект пульсации для внешней пилюли */
|
||||
.outer-pill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg,
|
||||
rgba(99, 102, 241, 0.3) 0%,
|
||||
rgba(139, 92, 246, 0.3) 100%);
|
||||
border-radius: 52px;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.outer-pill.expanded::after {
|
||||
opacity: 1;
|
||||
animation: pillGlow 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pillShimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pillGlow {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmerSlide {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
@@ -1,109 +1,45 @@
|
||||
// Modern Scripts for SmartSolTech Website
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('SmartSolTech: DOM loaded, initializing...');
|
||||
|
||||
// Hide loading screen
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) {
|
||||
console.log('SmartSolTech: Loading screen found, hiding...');
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.opacity = '0';
|
||||
loadingScreen.style.pointerEvents = 'none';
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.display = 'none';
|
||||
// Полностью удаляем элемент из DOM
|
||||
if (loadingScreen.parentNode) {
|
||||
loadingScreen.parentNode.removeChild(loadingScreen);
|
||||
console.log('SmartSolTech: Loading screen completely removed from DOM');
|
||||
}
|
||||
}, 300);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Theme Toggle Functionality
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Check for saved theme preference
|
||||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||||
html.setAttribute('data-theme', currentTheme);
|
||||
updateThemeIcon(currentTheme);
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// Add animation
|
||||
this.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
});
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
const icon = themeToggle.querySelector('i');
|
||||
if (theme === 'dark') {
|
||||
icon.className = 'fas fa-sun';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на светлую тему');
|
||||
} else {
|
||||
icon.className = 'fas fa-moon';
|
||||
themeToggle.setAttribute('aria-label', 'Переключить на темную тему');
|
||||
}
|
||||
}, 200); // Уменьшили время ожидания до 200ms
|
||||
} else {
|
||||
console.log('SmartSolTech: Loading screen not found');
|
||||
}
|
||||
|
||||
// Navbar scroll behavior
|
||||
const navbar = document.querySelector('.navbar-modern');
|
||||
let lastScrollTop = 0;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Add/remove scrolled class
|
||||
if (scrollTop > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
});
|
||||
|
||||
// Scroll to top button
|
||||
const scrollToTopBtn = document.getElementById('scroll-to-top');
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.pageYOffset > 300) {
|
||||
scrollToTopBtn.style.display = 'block';
|
||||
scrollToTopBtn.style.opacity = '1';
|
||||
} else {
|
||||
scrollToTopBtn.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (window.pageYOffset <= 300) {
|
||||
scrollToTopBtn.style.display = 'none';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
scrollToTopBtn.addEventListener('click', function() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
if (navbar) {
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
anchor.addEventListener('click', function (e) {
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
const offsetTop = target.offsetTop - 80; // Account for fixed navbar
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
@@ -116,211 +52,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function(entries) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-fade-in-up');
|
||||
// Add stagger delay for child elements
|
||||
const children = entry.target.querySelectorAll('.service-card, .feature-list > *, .step-card');
|
||||
children.forEach((child, index) => {
|
||||
setTimeout(() => {
|
||||
child.classList.add('animate-fade-in-up');
|
||||
}, index * 100);
|
||||
});
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe elements for animation
|
||||
document.querySelectorAll('.service-card, .card-modern, .step-card').forEach(el => {
|
||||
observer.observe(el);
|
||||
// Observe cards and service items
|
||||
document.querySelectorAll('.card-modern, .service-card, .step-card').forEach(card => {
|
||||
observer.observe(card);
|
||||
});
|
||||
|
||||
// Form enhancements
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
const originalContent = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Отправляем...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Re-enable after 3 seconds (in case of slow response)
|
||||
setTimeout(() => {
|
||||
submitBtn.innerHTML = originalContent;
|
||||
submitBtn.disabled = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parallax effect for hero section
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrolled = window.pageYOffset;
|
||||
const parallaxElements = document.querySelectorAll('.animate-float');
|
||||
|
||||
parallaxElements.forEach(element => {
|
||||
const speed = 0.5;
|
||||
element.style.transform = `translateY(${scrolled * speed}px)`;
|
||||
});
|
||||
});
|
||||
|
||||
// Service cards hover effect
|
||||
document.querySelectorAll('.service-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-10px) scale(1.02)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0) scale(1)';
|
||||
});
|
||||
});
|
||||
|
||||
// Card modern hover effects
|
||||
document.querySelectorAll('.card-modern').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.boxShadow = '0 25px 50px -12px rgba(0, 0, 0, 0.25)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.boxShadow = 'var(--shadow)';
|
||||
});
|
||||
});
|
||||
|
||||
// Add loading animation to buttons
|
||||
document.querySelectorAll('.btn-primary-modern, .btn-secondary-modern').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
// Create ripple effect
|
||||
const ripple = document.createElement('span');
|
||||
const rect = this.getBoundingClientRect();
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2;
|
||||
const y = e.clientY - rect.top - size / 2;
|
||||
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s linear;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
left: ${x}px;
|
||||
top: ${y}px;
|
||||
`;
|
||||
|
||||
this.style.position = 'relative';
|
||||
this.style.overflow = 'hidden';
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.remove();
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Typing animation for hero text (optional)
|
||||
const typingText = document.querySelector('.typing-text');
|
||||
if (typingText) {
|
||||
const text = typingText.textContent;
|
||||
typingText.textContent = '';
|
||||
let i = 0;
|
||||
|
||||
function typeWriter() {
|
||||
if (i < text.length) {
|
||||
typingText.textContent += text.charAt(i);
|
||||
i++;
|
||||
setTimeout(typeWriter, 100);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(typeWriter, 1000);
|
||||
}
|
||||
|
||||
// Mobile menu enhancements
|
||||
const navbarToggler = document.querySelector('.navbar-toggler');
|
||||
const navbarCollapse = document.querySelector('.navbar-collapse');
|
||||
|
||||
if (navbarToggler && navbarCollapse) {
|
||||
navbarToggler.addEventListener('click', function() {
|
||||
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Animate the toggler icon
|
||||
this.style.transform = 'rotate(180deg)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'rotate(0deg)';
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Close menu when clicking on a link
|
||||
document.querySelectorAll('.navbar-nav .nav-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const bsCollapse = new bootstrap.Collapse(navbarCollapse, {
|
||||
hide: true
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Newsletter form
|
||||
const newsletterForm = document.querySelector('footer form');
|
||||
if (newsletterForm) {
|
||||
newsletterForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const email = this.querySelector('input[type="email"]').value;
|
||||
|
||||
if (email) {
|
||||
// Show success message
|
||||
const button = this.querySelector('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
button.style.background = '#10b981';
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
button.style.background = '';
|
||||
this.reset();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add CSS for ripple animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.navbar-modern {
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card, .card-modern {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.step-card:nth-child(even) {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
console.log('SmartSolTech: All scripts loaded successfully');
|
||||
});
|
||||
@@ -3,12 +3,12 @@
|
||||
"name": "Smartsoltech",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/img/photo_2024-10-06_10-06-08.jpg",
|
||||
"src": "/static/img/logo.jpg",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "1011x702"
|
||||
},
|
||||
{
|
||||
"src": "/static/img/photo_2024-10-06_10-06-08.jpg",
|
||||
"src": "/static/img/logo.jpg",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "1011x702"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.contrib import admin
|
||||
from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo, Team, Career
|
||||
from .models import (
|
||||
Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest,
|
||||
HeroBanner, ContactInfo, Team, Career,
|
||||
ProjectMedia
|
||||
)
|
||||
from .forms import ProjectForm
|
||||
|
||||
@admin.register(ContactInfo)
|
||||
@@ -30,20 +34,6 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||
has_video.boolean = True
|
||||
has_video.short_description = 'Есть видео'
|
||||
|
||||
@admin.register(Project)
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
form = ProjectForm
|
||||
list_display = ('name', 'client','service', 'status', 'order', 'has_video')
|
||||
list_filter = ('name', 'client','service', 'status', 'order')
|
||||
search_fields = ('name', 'client','service', 'status', 'order', 'client__first_name', 'client__last_name')
|
||||
fields = ('name', 'description', 'completion_date', 'client', 'service', 'order',
|
||||
'category', 'image', 'video', 'video_poster', 'status')
|
||||
|
||||
def has_video(self, obj):
|
||||
return bool(obj.video)
|
||||
has_video.boolean = True
|
||||
has_video.short_description = 'Есть видео'
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'email', 'phone_number')
|
||||
@@ -80,8 +70,28 @@ class BlogPostAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name','description')
|
||||
search_fields = ('name',)
|
||||
list_display = ('name', 'slug', 'order', 'is_active', 'services_count', 'projects_count')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('name', 'description')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
list_editable = ('order', 'is_active')
|
||||
ordering = ('order', 'name')
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('name', 'slug', 'description', 'icon')
|
||||
}),
|
||||
('Настройки отображения', {
|
||||
'fields': ('order', 'is_active')
|
||||
}),
|
||||
)
|
||||
|
||||
def services_count(self, obj):
|
||||
return obj.services.count()
|
||||
services_count.short_description = 'Услуг'
|
||||
|
||||
def projects_count(self, obj):
|
||||
return obj.projects.count()
|
||||
projects_count.short_description = 'Проектов'
|
||||
|
||||
@admin.register(ServiceRequest)
|
||||
class ServiceRequestAdmin(admin.ModelAdmin):
|
||||
@@ -179,4 +189,105 @@ class CareerAdmin(admin.ModelAdmin):
|
||||
def mark_as_closed(self, request, queryset):
|
||||
updated = queryset.update(status='closed')
|
||||
self.message_user(request, f'{updated} вакансий закрыты.')
|
||||
mark_as_closed.short_description = "Закрыть"
|
||||
mark_as_closed.short_description = "Закрыть"
|
||||
|
||||
|
||||
# ============================================
|
||||
# ПРОЕКТЫ - АДМИНКИ
|
||||
# ============================================
|
||||
|
||||
class ProjectMediaInline(admin.TabularInline):
|
||||
"""Inline для медиа-файлов проекта"""
|
||||
model = ProjectMedia
|
||||
extra = 1
|
||||
fields = ('media_type', 'image', 'video', 'video_poster', 'embed_code', 'caption', 'order')
|
||||
ordering = ('order',)
|
||||
|
||||
|
||||
@admin.register(Project)
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
"""Админка для проектов"""
|
||||
list_display = ('name', 'status', 'is_featured', 'display_order', 'categories_display',
|
||||
'views_count', 'likes_count', 'media_count', 'completion_date')
|
||||
list_filter = ('status', 'is_featured', 'categories', 'completion_date')
|
||||
search_fields = ('name', 'description', 'client__first_name', 'client__last_name', 'technologies')
|
||||
filter_horizontal = ('categories',)
|
||||
list_editable = ('is_featured', 'display_order', 'status')
|
||||
ordering = ('-is_featured', '-display_order', '-completion_date')
|
||||
date_hierarchy = 'completion_date'
|
||||
|
||||
inlines = [ProjectMediaInline]
|
||||
|
||||
fieldsets = (
|
||||
('📋 Основная информация', {
|
||||
'fields': ('name', 'categories', 'status', 'is_featured', 'display_order')
|
||||
}),
|
||||
('📝 Описание', {
|
||||
'fields': ('short_description', 'description', 'image')
|
||||
}),
|
||||
('🏢 Детали проекта', {
|
||||
'fields': ('client', 'service', 'order', 'category', 'project_url', 'github_url',
|
||||
'technologies', 'duration', 'team_size', 'completion_date')
|
||||
}),
|
||||
('🎬 Видео', {
|
||||
'fields': ('video', 'video_poster'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('🔍 SEO', {
|
||||
'fields': ('meta_title', 'meta_description', 'meta_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('📊 Статистика', {
|
||||
'fields': ('views_count', 'likes_count'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ('views_count', 'likes_count')
|
||||
|
||||
def categories_display(self, obj):
|
||||
return ', '.join([cat.name for cat in obj.categories.all()[:3]])
|
||||
categories_display.short_description = 'Категории'
|
||||
|
||||
def media_count(self, obj):
|
||||
return obj.media_files.count()
|
||||
media_count.short_description = 'Медиа'
|
||||
|
||||
actions = ['mark_as_completed', 'mark_as_featured']
|
||||
|
||||
def mark_as_completed(self, request, queryset):
|
||||
updated = queryset.update(status='completed')
|
||||
self.message_user(request, f'{updated} проектов отмечены как завершённые.')
|
||||
mark_as_completed.short_description = "Отметить как завершённые"
|
||||
|
||||
def mark_as_featured(self, request, queryset):
|
||||
updated = queryset.update(is_featured=True)
|
||||
self.message_user(request, f'{updated} проектов отмечены как избранные.')
|
||||
mark_as_featured.short_description = "Отметить как избранные"
|
||||
|
||||
|
||||
@admin.register(ProjectMedia)
|
||||
class ProjectMediaAdmin(admin.ModelAdmin):
|
||||
"""Админка для медиа-файлов проектов"""
|
||||
list_display = ('id', 'project', 'media_type', 'caption', 'order', 'uploaded_at')
|
||||
list_filter = ('media_type', 'uploaded_at')
|
||||
search_fields = ('project__name', 'caption', 'alt_text')
|
||||
list_editable = ('order',)
|
||||
ordering = ('project', 'order', '-uploaded_at')
|
||||
|
||||
fieldsets = (
|
||||
('Проект', {
|
||||
'fields': ('project', 'media_type', 'order')
|
||||
}),
|
||||
('Изображение', {
|
||||
'fields': ('image', 'alt_text'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Видео', {
|
||||
'fields': ('video', 'video_poster', 'embed_code'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Описание', {
|
||||
'fields': ('caption',)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-25 23:21
|
||||
|
||||
import ckeditor_uploader.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0013_career_team'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PortfolioCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('icon', models.CharField(blank=True, help_text='Класс иконки (например: fa-code)', max_length=50, verbose_name='Иконка')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Категория портфолио',
|
||||
'verbose_name_plural': 'Категории портфолио',
|
||||
'ordering': ['order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpost',
|
||||
name='content',
|
||||
field=ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NewsPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='Заголовок')),
|
||||
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
|
||||
('excerpt', models.TextField(max_length=300, verbose_name='Краткое описание')),
|
||||
('content', ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание')),
|
||||
('featured_image', models.ImageField(upload_to='news/', verbose_name='Главное изображение')),
|
||||
('tags', models.CharField(blank=True, help_text='Разделите запятыми', max_length=200, verbose_name='Теги')),
|
||||
('is_published', models.BooleanField(default=False, verbose_name='Опубликовано')),
|
||||
('is_featured', models.BooleanField(default=False, verbose_name='Избранная новость')),
|
||||
('views_count', models.PositiveIntegerField(default=0, verbose_name='Просмотры')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата публикации')),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='web.category', verbose_name='Категория')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Новость',
|
||||
'verbose_name_plural': 'Новости',
|
||||
'ordering': ['-published_at', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PortfolioItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='Название проекта')),
|
||||
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
|
||||
('short_description', models.TextField(max_length=300, verbose_name='Краткое описание')),
|
||||
('description', ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Полное описание')),
|
||||
('thumbnail', models.ImageField(upload_to='portfolio/thumbnails/', verbose_name='Превью изображение')),
|
||||
('client', models.CharField(blank=True, max_length=200, verbose_name='Клиент')),
|
||||
('project_url', models.URLField(blank=True, verbose_name='Ссылка на проект')),
|
||||
('github_url', models.URLField(blank=True, verbose_name='GitHub репозиторий')),
|
||||
('technologies', models.TextField(help_text='Разделите запятыми', verbose_name='Технологии')),
|
||||
('duration', models.CharField(blank=True, max_length=100, verbose_name='Длительность')),
|
||||
('team_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер команды')),
|
||||
('status', models.CharField(choices=[('draft', 'Черновик'), ('published', 'Опубликовано'), ('featured', 'Избранное')], default='draft', max_length=20, verbose_name='Статус')),
|
||||
('completion_date', models.DateField(blank=True, null=True, verbose_name='Дата завершения')),
|
||||
('meta_title', models.CharField(blank=True, max_length=200, verbose_name='SEO заголовок')),
|
||||
('meta_description', models.TextField(blank=True, max_length=300, verbose_name='SEO описание')),
|
||||
('views_count', models.PositiveIntegerField(default=0, verbose_name='Просмотры')),
|
||||
('likes_count', models.PositiveIntegerField(default=0, verbose_name='Лайки')),
|
||||
('is_featured', models.BooleanField(default=False, verbose_name='Избранный проект')),
|
||||
('display_order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата публикации')),
|
||||
('categories', models.ManyToManyField(related_name='portfolio_items', to='web.portfoliocategory', verbose_name='Категории')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Проект портфолио',
|
||||
'verbose_name_plural': 'Портфолио',
|
||||
'ordering': ['-is_featured', '-display_order', '-published_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PortfolioMedia',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('media_type', models.CharField(choices=[('image', 'Изображение'), ('video', 'Видео'), ('embed', 'Встроенное видео (YouTube, Vimeo)')], default='image', max_length=10, verbose_name='Тип медиа')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='portfolio/gallery/', verbose_name='Изображение')),
|
||||
('video', models.FileField(blank=True, null=True, upload_to='portfolio/videos/', verbose_name='Видео файл')),
|
||||
('video_poster', models.ImageField(blank=True, null=True, upload_to='portfolio/posters/', verbose_name='Превью видео')),
|
||||
('embed_url', models.URLField(blank=True, verbose_name='URL видео (YouTube, Vimeo)')),
|
||||
('caption', models.CharField(blank=True, max_length=200, verbose_name='Подпись')),
|
||||
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alt текст')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
||||
('portfolio_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_files', to='web.portfolioitem', verbose_name='Проект')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Медиа файл портфолио',
|
||||
'verbose_name_plural': 'Медиа файлы портфолио',
|
||||
'ordering': ['order', 'uploaded_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,217 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-26 00:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import tinymce.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0014_portfoliocategory_alter_blogpost_content_newspost_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProjectCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('icon', models.CharField(blank=True, help_text='Класс иконки (например: fa-code)', max_length=50, verbose_name='Иконка')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Категория проекта',
|
||||
'verbose_name_plural': 'Категории проектов',
|
||||
'ordering': ['order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='portfolioitem',
|
||||
name='categories',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='portfoliomedia',
|
||||
name='portfolio_item',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='project',
|
||||
options={'ordering': ['-is_featured', '-display_order', '-completion_date'], 'verbose_name': 'Проект', 'verbose_name_plural': 'Проекты'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='display_order',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Порядок отображения'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='duration',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Длительность'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='github_url',
|
||||
field=models.URLField(blank=True, verbose_name='GitHub репозиторий'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='is_featured',
|
||||
field=models.BooleanField(default=False, verbose_name='Избранный проект'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='likes_count',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Количество лайков'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_description',
|
||||
field=models.TextField(blank=True, max_length=300, verbose_name='SEO описание'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_keywords',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Ключевые слова'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_title',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='SEO заголовок'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='project_url',
|
||||
field=models.URLField(blank=True, verbose_name='Ссылка на проект'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='short_description',
|
||||
field=models.TextField(default='Описание проекта', max_length=300, verbose_name='Краткое описание'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='team_size',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер команды'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='technologies',
|
||||
field=models.TextField(blank=True, help_text='Разделите запятыми', verbose_name='Технологии'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='thumbnail',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='static/img/project/thumbnails/', verbose_name='Миниатюра'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='views_count',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Количество просмотров'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpost',
|
||||
name='content',
|
||||
field=tinymce.models.HTMLField(verbose_name='Содержание'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='web.category', verbose_name='Категория (старая)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='client',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='web.client', verbose_name='Клиент'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='completion_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата завершения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='description',
|
||||
field=tinymce.models.HTMLField(verbose_name='Полное описание'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='static/img/project/', verbose_name='Главное изображение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, verbose_name='Название проекта'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='order',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project', to='web.order', verbose_name='Заказ'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='service',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='web.service', verbose_name='Услуга'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_progress', 'В процессе'), ('completed', 'Завершен'), ('archived', 'В архиве')], default='in_progress', max_length=50, verbose_name='Статус'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(blank=True, related_name='projects', to='web.projectcategory', verbose_name='Категории'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectMedia',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('media_type', models.CharField(choices=[('image', 'Изображение'), ('video', 'Видео'), ('embed_video', 'Встроенное видео (YouTube, Vimeo)')], default='image', max_length=20, verbose_name='Тип медиа')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='static/img/project/gallery/', verbose_name='Изображение')),
|
||||
('video', models.FileField(blank=True, null=True, upload_to='static/video/project/gallery/', verbose_name='Видео файл')),
|
||||
('video_poster', models.ImageField(blank=True, null=True, upload_to='static/img/project/gallery/posters/', verbose_name='Превью видео')),
|
||||
('embed_code', models.TextField(blank=True, help_text='Вставьте iframe код от YouTube или Vimeo', verbose_name='Код встраивания (iframe)')),
|
||||
('caption', models.CharField(blank=True, max_length=200, verbose_name='Подпись')),
|
||||
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alt текст')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_items', to='web.project', verbose_name='Проект')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Медиа файл проекта',
|
||||
'verbose_name_plural': 'Медиа файлы проектов',
|
||||
'ordering': ['order', 'uploaded_at'],
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='NewsPost',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioCategory',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioItem',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioMedia',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-26 00:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0015_projectcategory_remove_portfolioitem_categories_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='ProjectCategory',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': ['order', 'name'], 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='icon',
|
||||
field=models.CharField(blank=True, help_text='Класс FontAwesome (например: fa-code)', max_length=50, verbose_name='Иконка'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='Активна'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Порядок'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, max_length=100, null=True, unique=True, verbose_name='URL'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(blank=True, related_name='projects', to='web.category', verbose_name='Категории'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectmedia',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_files', to='web.project', verbose_name='Проект'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser, User
|
||||
from tinymce.models import HTMLField
|
||||
import uuid
|
||||
from django.urls import reverse
|
||||
|
||||
@@ -54,15 +55,25 @@ class HeroBanner(models.Model):
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True, blank=True, null=True, verbose_name="URL")
|
||||
description = models.TextField(default='Описание категории')
|
||||
icon = models.CharField(max_length=50, blank=True, verbose_name="Иконка", help_text="Класс FontAwesome (например: fa-code)")
|
||||
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
||||
is_active = models.BooleanField(default=True, verbose_name="Активна")
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Категория'
|
||||
verbose_name_plural = 'Категории'
|
||||
ordering = ['name']
|
||||
ordering = ['order', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
from django.utils.text import slugify
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Service(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
@@ -111,7 +122,7 @@ class Client(models.Model):
|
||||
|
||||
class BlogPost(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
content = models.TextField()
|
||||
content = HTMLField(verbose_name="Содержание")
|
||||
published_date = models.DateTimeField(auto_now_add=True)
|
||||
image = models.ImageField(upload_to='static/img/blog/', blank=True, null=True)
|
||||
video = models.FileField(upload_to='static/video/blog/', blank=True, null=True, help_text='Видео файл для блог поста')
|
||||
@@ -172,26 +183,199 @@ class Order(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('order_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
# ПРОЕКТЫ
|
||||
# ============================================
|
||||
|
||||
class Project(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(default='Описание проекта')
|
||||
completion_date = models.DateField(blank=True, null=True)
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='projects')
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='projects')
|
||||
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='project', null=True, blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
image = models.ImageField(upload_to='static/img/project/', blank=True, null=True)
|
||||
"""Расширенная модель проекта с множественными категориями и медиа"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('in_progress', 'В процессе'),
|
||||
('completed', 'Завершен'),
|
||||
('archived', 'В архиве'),
|
||||
]
|
||||
|
||||
# Основная информация
|
||||
name = models.CharField(max_length=200, verbose_name="Название проекта")
|
||||
slug = models.SlugField(max_length=200, unique=True, verbose_name="URL", blank=True)
|
||||
|
||||
# Краткое описание для списка
|
||||
short_description = models.TextField(max_length=300, verbose_name="Краткое описание", default='Описание проекта')
|
||||
|
||||
# Полное описание с WYSIWYG редактором
|
||||
description = HTMLField(verbose_name="Полное описание")
|
||||
|
||||
# Связи с существующими моделями
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='projects', verbose_name="Клиент")
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='projects', verbose_name="Услуга")
|
||||
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='project', null=True, blank=True, verbose_name="Заказ")
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Категория (старая)")
|
||||
|
||||
# Множественные категории проектов = категории услуг
|
||||
categories = models.ManyToManyField(Category, related_name='projects', verbose_name="Категории", blank=True)
|
||||
|
||||
# Главное изображение (для обратной совместимости и превью)
|
||||
image = models.ImageField(upload_to='static/img/project/', blank=True, null=True, verbose_name="Главное изображение")
|
||||
thumbnail = models.ImageField(upload_to='static/img/project/thumbnails/', blank=True, null=True, verbose_name="Миниатюра")
|
||||
|
||||
# Видео (для обратной совместимости)
|
||||
video = models.FileField(upload_to='static/video/project/', blank=True, null=True, help_text='Видео презентация проекта')
|
||||
video_poster = models.ImageField(upload_to='static/img/project/posters/', blank=True, null=True, help_text='Превью изображение для видео проекта')
|
||||
status = models.CharField(max_length=50, choices=[('in_progress', 'В процессе'), ('completed', 'Завершен')], default='in_progress')
|
||||
|
||||
# Дополнительная информация о проекте
|
||||
project_url = models.URLField(blank=True, verbose_name="Ссылка на проект")
|
||||
github_url = models.URLField(blank=True, verbose_name="GitHub репозиторий")
|
||||
|
||||
# Технологии и инструменты
|
||||
technologies = models.TextField(blank=True, verbose_name="Технологии", help_text="Разделите запятыми")
|
||||
|
||||
# Метрики проекта
|
||||
duration = models.CharField(max_length=100, blank=True, verbose_name="Длительность")
|
||||
team_size = models.PositiveIntegerField(blank=True, null=True, verbose_name="Размер команды")
|
||||
|
||||
# Даты и статус
|
||||
completion_date = models.DateField(blank=True, null=True, verbose_name="Дата завершения")
|
||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='in_progress', verbose_name="Статус")
|
||||
|
||||
# Счетчики и метрики
|
||||
views_count = models.PositiveIntegerField(default=0, verbose_name='Количество просмотров')
|
||||
likes_count = models.PositiveIntegerField(default=0, verbose_name='Количество лайков')
|
||||
|
||||
# Настройки отображения
|
||||
is_featured = models.BooleanField(default=False, verbose_name="Избранный проект")
|
||||
display_order = models.PositiveIntegerField(default=0, verbose_name="Порядок отображения")
|
||||
|
||||
# SEO
|
||||
meta_title = models.CharField(max_length=200, blank=True, verbose_name="SEO заголовок")
|
||||
meta_description = models.TextField(max_length=300, blank=True, verbose_name="SEO описание")
|
||||
meta_keywords = models.CharField(max_length=200, blank=True, verbose_name="Ключевые слова")
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Проект'
|
||||
verbose_name_plural = 'Проекты'
|
||||
ordering = ['-completion_date']
|
||||
ordering = ['-is_featured', '-display_order', '-completion_date']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('project_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def technologies_list(self):
|
||||
"""Возвращает список технологий"""
|
||||
if self.technologies:
|
||||
return [tech.strip() for tech in self.technologies.split(',') if tech.strip()]
|
||||
return []
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
from django.utils.text import slugify
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
# Автоматически создаем thumbnail из главного изображения
|
||||
if self.image and not self.thumbnail:
|
||||
self.thumbnail = self.image
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Ресайз thumbnail после сохранения
|
||||
if self.thumbnail:
|
||||
self._resize_thumbnail()
|
||||
|
||||
def _resize_thumbnail(self):
|
||||
"""Автоматический ресайз thumbnail до 600x400px"""
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from django.core.files.base import ContentFile
|
||||
import os
|
||||
|
||||
if not self.thumbnail:
|
||||
return
|
||||
|
||||
try:
|
||||
# Открываем изображение
|
||||
img = Image.open(self.thumbnail.path)
|
||||
|
||||
# Конвертируем в RGB если нужно
|
||||
if img.mode not in ('RGB', 'RGBA'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Целевой размер
|
||||
target_width = 600
|
||||
target_height = 400
|
||||
|
||||
# Вычисляем соотношение сторон
|
||||
img_ratio = img.width / img.height
|
||||
target_ratio = target_width / target_height
|
||||
|
||||
# Обрезаем изображение по центру
|
||||
if img_ratio > target_ratio:
|
||||
# Изображение шире, обрезаем по ширине
|
||||
new_width = int(img.height * target_ratio)
|
||||
left = (img.width - new_width) // 2
|
||||
img = img.crop((left, 0, left + new_width, img.height))
|
||||
else:
|
||||
# Изображение выше, обрезаем по высоте
|
||||
new_height = int(img.width / target_ratio)
|
||||
top = (img.height - new_height) // 2
|
||||
img = img.crop((0, top, img.width, top + new_height))
|
||||
|
||||
# Ресайз до целевого размера
|
||||
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Сохраняем оптимизированное изображение
|
||||
img.save(self.thumbnail.path, quality=85, optimize=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при ресайзе thumbnail для проекта {self.name}: {e}")
|
||||
|
||||
|
||||
class ProjectMedia(models.Model):
|
||||
"""Медиа-файлы для проектов (множественные фото и видео)"""
|
||||
|
||||
MEDIA_TYPE_CHOICES = [
|
||||
('image', 'Изображение'),
|
||||
('video', 'Видео'),
|
||||
('embed_video', 'Встроенное видео (YouTube, Vimeo)'),
|
||||
]
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='media_files', verbose_name="Проект")
|
||||
|
||||
media_type = models.CharField(max_length=20, choices=MEDIA_TYPE_CHOICES, default='image', verbose_name="Тип медиа")
|
||||
|
||||
# Для изображений
|
||||
image = models.ImageField(upload_to='static/img/project/gallery/', blank=True, null=True, verbose_name="Изображение")
|
||||
|
||||
# Для видео
|
||||
video = models.FileField(upload_to='static/video/project/gallery/', blank=True, null=True, verbose_name="Видео файл")
|
||||
video_poster = models.ImageField(upload_to='static/img/project/gallery/posters/', blank=True, null=True, verbose_name="Превью видео")
|
||||
|
||||
# Для встроенных видео
|
||||
embed_code = models.TextField(blank=True, verbose_name="Код встраивания (iframe)", help_text="Вставьте iframe код от YouTube или Vimeo")
|
||||
|
||||
# Описание и метаданные
|
||||
caption = models.CharField(max_length=200, blank=True, verbose_name="Подпись")
|
||||
alt_text = models.CharField(max_length=200, blank=True, verbose_name="Alt текст")
|
||||
|
||||
# Порядок отображения
|
||||
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
||||
|
||||
# Timestamps
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Медиа файл проекта'
|
||||
verbose_name_plural = 'Медиа файлы проектов'
|
||||
ordering = ['order', 'uploaded_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_media_type_display()} для {self.project.name}"
|
||||
|
||||
class Review(models.Model):
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='reviews')
|
||||
@@ -382,4 +566,3 @@ class Career(models.Model):
|
||||
if self.application_deadline and self.application_deadline < timezone.now().date():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -72,11 +72,6 @@
|
||||
<!-- Footer -->
|
||||
{% include 'web/footer_modern.html' %}
|
||||
|
||||
<!-- Theme Toggle Button -->
|
||||
<button id="theme-toggle" class="theme-toggle" aria-label="Переключить тему">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Scroll to Top Button -->
|
||||
<button id="scroll-to-top" class="position-fixed bottom-0 end-0 m-4 btn btn-primary-modern rounded-circle" style="width: 50px; height: 50px; display: none; z-index: 999;">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
|
||||
@@ -65,11 +65,6 @@
|
||||
О нас
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#" class="text-light opacity-75 text-decoration-none hover-primary">
|
||||
Портфолио
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#" class="text-light opacity-75 text-decoration-none hover-primary">
|
||||
Команда
|
||||
|
||||
@@ -65,11 +65,6 @@
|
||||
О нас
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#" class="text-light opacity-75 text-decoration-none hover-primary">
|
||||
Портфолио
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#" class="text-light opacity-75 text-decoration-none hover-primary">
|
||||
Команда
|
||||
|
||||
@@ -404,7 +404,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<a href="{% url 'portfolio' %}" class="btn btn-primary-modern btn-lg">
|
||||
<a href="{% url 'projects_list' %}" class="btn btn-primary-modern btn-lg">
|
||||
<i class="fas fa-th-large me-2"></i>
|
||||
Смотреть все проекты
|
||||
</a>
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
<i class="fas fa-cog me-2"></i>Услуги
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link-modern {% if 'project' in request.resolver_match.url_name %}active{% endif %}"
|
||||
href="{% url 'projects_list' %}">
|
||||
<i class="fas fa-briefcase me-2"></i>Проекты
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link-modern {% if request.resolver_match.url_name == 'about' %}active{% endif %}"
|
||||
href="{% url 'about' %}">
|
||||
@@ -40,7 +46,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link-modern {% if request.resolver_match.url_name == 'career' %}active{% endif %}"
|
||||
href="{% url 'career' %}">
|
||||
<i class="fas fa-briefcase me-2"></i>Карьера
|
||||
<i class="fas fa-user-tie me-2"></i>Карьера
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
||||
305
smartsoltech/web/templates/web/portfolio_detail.html
Normal file
305
smartsoltech/web/templates/web/portfolio_detail.html
Normal file
@@ -0,0 +1,305 @@
|
||||
{% extends 'web/base_modern.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ portfolio.title }} - Портфолио - SmartSolTech{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css">
|
||||
<style>
|
||||
.portfolio-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 100px 0 60px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.portfolio-gallery {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.swiper-slide img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.portfolio-info {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.portfolio-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.portfolio-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.tech-badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
margin: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.similar-project {
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
background: white;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.similar-project:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.similar-thumb {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.similar-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.similar-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="portfolio-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3">{{ portfolio.title }}</h1>
|
||||
<p class="lead mb-3">{{ portfolio.short_description }}</p>
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
{% for category in portfolio.categories.all %}
|
||||
<span class="badge bg-light text-dark px-3 py-2">
|
||||
<i class="{{ category.icon }} me-2"></i>{{ category.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end mt-4 mt-lg-0">
|
||||
<div class="d-flex justify-content-lg-end gap-3">
|
||||
{% if portfolio.project_url %}
|
||||
<a href="{{ portfolio.project_url }}" target="_blank" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-external-link-alt me-2"></i>Посетить сайт
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if portfolio.github_url %}
|
||||
<a href="{{ portfolio.github_url }}" target="_blank" class="btn btn-outline-light btn-lg">
|
||||
<i class="fab fa-github me-2"></i>GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
{% if media_items %}
|
||||
<div class="portfolio-gallery">
|
||||
<div class="swiper portfolioSwiper">
|
||||
<div class="swiper-wrapper">
|
||||
{% for media in media_items %}
|
||||
{% if media.media_type == 'image' %}
|
||||
<div class="swiper-slide">
|
||||
<a href="{{ media.image.url }}" data-lightbox="portfolio-{{ portfolio.id }}" data-title="{{ media.caption }}">
|
||||
<img src="{{ media.image.url }}" alt="{{ media.alt_text }}">
|
||||
</a>
|
||||
</div>
|
||||
{% elif media.media_type == 'video' %}
|
||||
<div class="swiper-slide">
|
||||
<video controls style="width:100%; height:100%; object-fit:cover;">
|
||||
<source src="{{ media.video.url }}" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
{% elif media.media_type == 'embed_video' %}
|
||||
<div class="swiper-slide">
|
||||
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
|
||||
{{ media.embed_code|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="swiper-button-next"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="portfolio-content">
|
||||
<h2 class="mb-4">Описание проекта</h2>
|
||||
{{ portfolio.description|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="portfolio-info">
|
||||
<h3 class="mb-4">Информация о проекте</h3>
|
||||
|
||||
{% if portfolio.client_name %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-user me-2"></i>Клиент
|
||||
</div>
|
||||
<div>{{ portfolio.client_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio.completion_date %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-calendar me-2"></i>Дата завершения
|
||||
</div>
|
||||
<div>{{ portfolio.completion_date|date:"d.m.Y" }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio.duration %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-clock me-2"></i>Длительность
|
||||
</div>
|
||||
<div>{{ portfolio.duration }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio.team_size %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-users me-2"></i>Размер команды
|
||||
</div>
|
||||
<div>{{ portfolio.team_size }} человек</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-chart-line me-2"></i>Статистика
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-eye me-1"></i> {{ portfolio.views_count }} просмотров<br>
|
||||
<i class="fas fa-heart me-1"></i> {{ portfolio.likes_count }} лайков
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if portfolio.technologies %}
|
||||
<div class="portfolio-info">
|
||||
<h3 class="mb-3">Технологии</h3>
|
||||
<div>
|
||||
{% for tech in portfolio.technologies.split %}
|
||||
<span class="tech-badge">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if similar_projects %}
|
||||
<div class="mt-5">
|
||||
<h2 class="mb-4">Похожие проекты</h2>
|
||||
<div class="row">
|
||||
{% for item in similar_projects %}
|
||||
<div class="col-md-4">
|
||||
<div class="similar-project">
|
||||
<div class="similar-thumb">
|
||||
{% if item.thumbnail %}
|
||||
<img src="{{ item.thumbnail.url }}" alt="{{ item.title }}">
|
||||
{% else %}
|
||||
<img src="{% static 'img/default-portfolio.jpg' %}" alt="{{ item.title }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="h6 mb-2">{{ item.title }}</h4>
|
||||
<p class="text-muted small mb-3">{{ item.short_description|truncatewords:10 }}</p>
|
||||
<a href="{% url 'portfolio_detail' item.slug %}" class="btn btn-sm btn-primary">
|
||||
Подробнее
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<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>
|
||||
const swiper = new Swiper('.portfolioSwiper', {
|
||||
loop: true,
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
},
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
autoplay: {
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
effect: 'fade',
|
||||
fadeEffect: {
|
||||
crossFade: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
211
smartsoltech/web/templates/web/portfolio_list.html
Normal file
211
smartsoltech/web/templates/web/portfolio_list.html
Normal file
@@ -0,0 +1,211 @@
|
||||
{% extends 'web/base_modern.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Портфолио - SmartSolTech{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
|
||||
<style>
|
||||
.portfolio-hero {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 100px 0 60px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin: 2rem 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-btn:hover,
|
||||
.category-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.portfolio-card {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.portfolio-thumb {
|
||||
height: 250px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.portfolio-card:hover .portfolio-thumb img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.portfolio-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-categories {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.portfolio-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.portfolio-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.featured-badge {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="portfolio-hero">
|
||||
<div class="container text-center">
|
||||
<h1 class="display-4 fw-bold mb-3">Наше Портфолио</h1>
|
||||
<p class="lead">Проекты, которыми мы гордимся</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="category-filter">
|
||||
<a href="{% url 'portfolio_list' %}" class="category-btn {% if not request.GET.category %}active{% endif %}">
|
||||
<i class="fas fa-th me-2"></i>Все проекты
|
||||
</a>
|
||||
{% for category in categories %}
|
||||
<a href="?category={{ category.slug }}" class="category-btn {% if request.GET.category == category.slug %}active{% endif %}">
|
||||
<i class="{{ category.icon }} me-2"></i>{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if featured %}
|
||||
<div class="mb-5">
|
||||
<h2 class="mb-4"><i class="fas fa-star text-warning me-2"></i>Избранные проекты</h2>
|
||||
<div class="row">
|
||||
{% for item in featured %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="portfolio-card">
|
||||
<div class="portfolio-thumb">
|
||||
<span class="featured-badge"><i class="fas fa-star me-1"></i>Избранное</span>
|
||||
{% if item.thumbnail %}
|
||||
<img src="{{ item.thumbnail.url }}" alt="{{ item.title }}">
|
||||
{% else %}
|
||||
<img src="{% static 'img/default-portfolio.jpg' %}" alt="{{ item.title }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="portfolio-content">
|
||||
<div class="portfolio-categories">
|
||||
{% for cat in item.categories.all %}
|
||||
<span class="portfolio-badge">{{ cat.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h3 class="h5 mb-2">{{ item.title }}</h3>
|
||||
<p class="text-muted mb-3">{{ item.short_description|truncatewords:20 }}</p>
|
||||
<div class="portfolio-stats mb-3">
|
||||
<span><i class="fas fa-eye me-1"></i>{{ item.views_count }}</span>
|
||||
<span><i class="fas fa-heart me-1"></i>{{ item.likes_count }}</span>
|
||||
<span><i class="fas fa-calendar me-1"></i>{{ item.completion_date|date:"Y" }}</span>
|
||||
</div>
|
||||
<a href="{% url 'portfolio_detail' item.slug %}" class="btn btn-primary">
|
||||
Подробнее <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="mb-4">Все проекты</h2>
|
||||
<div class="row">
|
||||
{% for item in portfolios %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="portfolio-card">
|
||||
<div class="portfolio-thumb">
|
||||
{% if item.thumbnail %}
|
||||
<img src="{{ item.thumbnail.url }}" alt="{{ item.title }}">
|
||||
{% else %}
|
||||
<img src="{% static 'img/default-portfolio.jpg' %}" alt="{{ item.title }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="portfolio-content">
|
||||
<div class="portfolio-categories">
|
||||
{% for cat in item.categories.all %}
|
||||
<span class="portfolio-badge">{{ cat.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h3 class="h5 mb-2">{{ item.title }}</h3>
|
||||
<p class="text-muted mb-3">{{ item.short_description|truncatewords:20 }}</p>
|
||||
<div class="portfolio-stats mb-3">
|
||||
<span><i class="fas fa-eye me-1"></i>{{ item.views_count }}</span>
|
||||
<span><i class="fas fa-heart me-1"></i>{{ item.likes_count }}</span>
|
||||
<span><i class="fas fa-calendar me-1"></i>{{ item.completion_date|date:"Y" }}</span>
|
||||
</div>
|
||||
<a href="{% url 'portfolio_detail' item.slug %}" class="btn btn-primary">
|
||||
Подробнее <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-folder-open fa-4x text-muted mb-3"></i>
|
||||
<h3 class="text-muted">Проектов пока нет</h3>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
444
smartsoltech/web/templates/web/project_detail.html
Normal file
444
smartsoltech/web/templates/web/project_detail.html
Normal file
@@ -0,0 +1,444 @@
|
||||
{% extends 'web/base_modern.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ project.name }} - Проекты - SmartSolTech{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css">
|
||||
<style>
|
||||
.project-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 100px 0 60px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.project-gallery {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.swiper-slide img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.swiper-slide video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.project-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.project-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.tech-badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
margin: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.similar-project {
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
background: white;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.similar-project:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.similar-thumb {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
|
||||
}
|
||||
|
||||
.similar-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.similar-project:hover .similar-thumb img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.similar-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.embed-responsive {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.embed-responsive iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 992px) {
|
||||
.project-info {
|
||||
position: static;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-header {
|
||||
padding: 80px 0 40px;
|
||||
}
|
||||
|
||||
.project-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.project-header .lead {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.project-content,
|
||||
.project-info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.similar-thumb {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.embed-responsive {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.project-header {
|
||||
padding: 60px 0 30px;
|
||||
}
|
||||
|
||||
.project-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
height: 250px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.tech-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
.project-gallery {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Swiper навигация для мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.swiper-button-next:after,
|
||||
.swiper-button-prev:after {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="project-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3">{{ project.name }}</h1>
|
||||
<p class="lead mb-3">{{ project.short_description }}</p>
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
{% for category in project.categories.all %}
|
||||
<span class="badge bg-light text-dark px-3 py-2">
|
||||
<i class="{{ category.icon }} me-2"></i>{{ category.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end mt-4 mt-lg-0">
|
||||
<div class="d-flex justify-content-lg-end gap-3 flex-wrap">
|
||||
{% if project.project_url %}
|
||||
<a href="{{ project.project_url }}" target="_blank" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-external-link-alt me-2"></i>Посетить сайт
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if project.github_url %}
|
||||
<a href="{{ project.github_url }}" target="_blank" class="btn btn-outline-light btn-lg">
|
||||
<i class="fab fa-github me-2"></i>GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
{% if project.media_files.exists %}
|
||||
<div class="project-gallery">
|
||||
<div class="swiper projectSwiper">
|
||||
<div class="swiper-wrapper">
|
||||
{% for media in project.media_files.all %}
|
||||
{% if media.media_type == 'image' %}
|
||||
<div class="swiper-slide">
|
||||
<a href="{{ media.image.url }}" data-lightbox="project-{{ project.id }}" data-title="{{ media.caption }}">
|
||||
<img src="{{ media.image.url }}" alt="{{ media.alt_text|default:project.name }}">
|
||||
</a>
|
||||
</div>
|
||||
{% elif media.media_type == 'video' %}
|
||||
<div class="swiper-slide">
|
||||
<video controls {% if media.video_poster %}poster="{{ media.video_poster.url }}"{% endif %}>
|
||||
<source src="{{ media.video.url }}" type="video/mp4">
|
||||
Ваш браузер не поддерживает видео.
|
||||
</video>
|
||||
</div>
|
||||
{% elif media.media_type == 'embed' and media.embed_url %}
|
||||
<div class="swiper-slide">
|
||||
<div class="embed-responsive">
|
||||
<iframe src="{{ media.embed_url }}" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="swiper-button-next"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="project-content">
|
||||
<h2 class="mb-4">Описание проекта</h2>
|
||||
{{ project.description|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="project-info">
|
||||
<h3 class="mb-4">Информация о проекте</h3>
|
||||
|
||||
{% if project.client %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-user me-2"></i>Клиент
|
||||
</div>
|
||||
<div>{{ project.client.name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.completion_date %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-calendar me-2"></i>Дата завершения
|
||||
</div>
|
||||
<div>{{ project.completion_date|date:"d.m.Y" }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.duration %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-clock me-2"></i>Длительность
|
||||
</div>
|
||||
<div>{{ project.duration }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.team_size %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-users me-2"></i>Размер команды
|
||||
</div>
|
||||
<div>{{ project.team_size }} человек</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-chart-line me-2"></i>Статистика
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-eye me-1"></i> {{ project.views_count }} просмотров<br>
|
||||
<i class="fas fa-heart me-1"></i> {{ project.likes_count }} лайков
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.status %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-info-circle me-2"></i>Статус
|
||||
</div>
|
||||
<div>
|
||||
{% if project.status == 'completed' %}
|
||||
<span class="badge bg-success">Завершён</span>
|
||||
{% elif project.status == 'in_progress' %}
|
||||
<span class="badge bg-warning">В процессе</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ project.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if project.technologies %}
|
||||
<div class="project-info">
|
||||
<h3 class="mb-3">Технологии</h3>
|
||||
<div>
|
||||
{% for tech in project.technologies_list %}
|
||||
<span class="tech-badge">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if similar_projects %}
|
||||
<div class="mt-5">
|
||||
<h2 class="mb-4">Похожие проекты</h2>
|
||||
<div class="row">
|
||||
{% for item in similar_projects %}
|
||||
<div class="col-md-4">
|
||||
<div class="similar-project">
|
||||
<div class="similar-thumb">
|
||||
{% if item.thumbnail %}
|
||||
<img src="{{ item.thumbnail.url }}" alt="{{ item.name }}">
|
||||
{% elif item.image %}
|
||||
<img src="{{ item.image.url }}" alt="{{ item.name }}">
|
||||
{% else %}
|
||||
<img src="{% static 'img/default-project.jpg' %}" alt="{{ item.name }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="similar-content">
|
||||
<h4 class="h6 mb-2">{{ item.name }}</h4>
|
||||
{% if item.short_description %}
|
||||
<p class="text-muted small mb-3">{{ item.short_description|truncatewords:10 }}</p>
|
||||
{% endif %}
|
||||
<a href="{% url 'project_detail' item.pk %}" class="btn btn-sm btn-primary">
|
||||
Подробнее
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<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>
|
||||
const swiper = new Swiper('.projectSwiper', {
|
||||
loop: true,
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
},
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
autoplay: {
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
},
|
||||
effect: 'fade',
|
||||
fadeEffect: {
|
||||
crossFade: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
362
smartsoltech/web/templates/web/projects_list.html
Normal file
362
smartsoltech/web/templates/web/projects_list.html
Normal file
@@ -0,0 +1,362 @@
|
||||
{% extends 'web/base_modern.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Наши проекты - SmartSolTech{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Projects Page v2.0 - Force Update */
|
||||
.projects-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 100px 0 60px;
|
||||
color: white;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
margin: 0.5rem 0.25rem;
|
||||
border-radius: 25px;
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
color: #4a5568;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
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 {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease !important;
|
||||
margin-bottom: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.2) !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.project-thumbnail {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
|
||||
}
|
||||
|
||||
.project-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.project-card:hover .project-thumbnail img {
|
||||
transform: scale(1.15) translateY(-10px) !important;
|
||||
}
|
||||
|
||||
.project-thumbnail::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.project-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 15px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
z-index: 1;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.project-content {
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
color: #718096;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-grow: 1;
|
||||
line-height: 1.5;
|
||||
font-size: 0.9rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.project-stats i {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.project-categories {
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
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.75rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-projects {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.no-projects i {
|
||||
font-size: 4rem;
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.projects-header {
|
||||
padding: 80px 0 40px;
|
||||
}
|
||||
|
||||
.projects-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.projects-header .lead {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.project-thumbnail {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.project-thumbnail {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
}
|
||||
|
||||
.project-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Скелетон загрузка */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="projects-header">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h1 class="display-4 fw-bold mb-3">Наши проекты</h1>
|
||||
<p class="lead mb-0">Портфолио завершённых работ и успешных внедрений</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% if categories %}
|
||||
<div class="category-filter">
|
||||
<a href="{% url 'projects_list' %}" class="category-btn {% if not selected_category %}active{% endif %}">
|
||||
<i class="fas fa-th me-2"></i>Все проекты
|
||||
</a>
|
||||
{% for category in categories %}
|
||||
<a href="{% url 'projects_list' %}?category={{ category.id }}"
|
||||
class="category-btn {% if selected_category == category.id|stringformat:'s' %}active{% endif %}">
|
||||
<i class="{{ category.icon }} me-2"></i>{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if projects %}
|
||||
<div class="projects-grid">
|
||||
{% for project in projects %}
|
||||
<a href="{% url 'project_detail' project.pk %}" class="text-decoration-none">
|
||||
<div class="project-card">
|
||||
<div class="project-thumbnail">
|
||||
{% if 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">
|
||||
{% else %}
|
||||
<img src="{% static 'img/default-project.jpg' %}" 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.categories.exists %}
|
||||
<div class="project-categories">
|
||||
{% for category in project.categories.all %}
|
||||
<span class="category-tag">
|
||||
<i class="{{ category.icon }} me-1"></i>{{ category.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.short_description %}
|
||||
<p class="project-description">{{ project.short_description|truncatewords:20 }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="project-meta">
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-projects">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<h3>Проектов не найдено</h3>
|
||||
<p>В данной категории пока нет завершённых проектов</p>
|
||||
<a href="{% url 'projects_list' %}" class="btn btn-primary-modern mt-3">
|
||||
<i class="fas fa-th me-2"></i>Посмотреть все проекты
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -224,32 +224,58 @@
|
||||
<div class="row g-4">
|
||||
{% for project in service.projects.all %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card-modern h-100">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<span class="badge bg-primary">{{ project.get_status_display }}</span>
|
||||
<a href="{% url 'project_detail' project.pk %}" class="text-decoration-none">
|
||||
<div class="card-modern h-100 hover-lift">
|
||||
{% if project.thumbnail %}
|
||||
<div style="height: 200px; overflow: hidden; border-radius: 15px 15px 0 0;">
|
||||
<img src="{{ project.thumbnail.url }}" alt="{{ project.name }}"
|
||||
style="width: 100%; height: 100%; object-fit: cover; object-position: center;"
|
||||
loading="lazy" decoding="async">
|
||||
</div>
|
||||
<h5 class="mb-3">{{ project.name }}</h5>
|
||||
<p class="text-muted mb-3">{{ project.description|truncatewords:15 }}</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<strong>Требования заказчика:</strong>
|
||||
<p class="text-muted small">{{ project.order.message|truncatewords:10 }}</p>
|
||||
{% elif project.image %}
|
||||
<div style="height: 200px; overflow: hidden; border-radius: 15px 15px 0 0;">
|
||||
<img src="{{ project.image.url }}" alt="{{ project.name }}"
|
||||
style="width: 100%; height: 100%; object-fit: cover; object-position: center;"
|
||||
loading="lazy" decoding="async">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-calendar-check me-1"></i>
|
||||
{{ project.completion_date|date:"d.m.Y" }}
|
||||
</small>
|
||||
<div class="project-rating">
|
||||
{% for i in "12345" %}
|
||||
<i class="fas fa-star text-warning"></i>
|
||||
{% endfor %}
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<span class="badge bg-primary">{{ project.get_status_display }}</span>
|
||||
{% if project.media_files.count > 0 %}
|
||||
<span class="badge bg-info ms-2">
|
||||
<i class="fas fa-images me-1"></i>{{ project.media_files.count }} фото
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h5 class="mb-3 text-dark">{{ project.name }}</h5>
|
||||
|
||||
{% if project.short_description %}
|
||||
<p class="text-muted mb-3">{{ project.short_description|truncatewords:15 }}</p>
|
||||
{% else %}
|
||||
<p class="text-muted mb-3">{{ project.description|striptags|truncatewords:15 }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-calendar-check me-1"></i>
|
||||
{% if project.completion_date %}
|
||||
{{ project.completion_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
Не указано
|
||||
{% endif %}
|
||||
</small>
|
||||
<div class="project-stats">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-eye me-1"></i>{{ project.views_count }}
|
||||
<i class="fas fa-heart ms-2 me-1"></i>{{ project.likes_count }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -510,6 +536,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Hover lift effect for project cards */
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Success Animation Styles */
|
||||
.success-checkmark {
|
||||
width: 80px;
|
||||
|
||||
@@ -8,10 +8,10 @@ urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('service/<int:pk>/', views.service_detail, name='service_detail'),
|
||||
path('project/<int:pk>/', views.project_detail, name='project_detail'),
|
||||
path('projects/', views.projects_list, name='projects_list'),
|
||||
path('client/<int:pk>/', views.client_detail, name='client_detail'),
|
||||
path('blog/<int:pk>/', views.blog_post_detail, name='blog_post_detail'),
|
||||
path('services/', views.services_view, name='services'),
|
||||
path('portfolio/', views.portfolio_view, name='portfolio'),
|
||||
path('blog/', views.blog_view, name='blog'),
|
||||
path('news/', views.news_view, name='news'),
|
||||
path('career/', views.career_view, name='career'),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from .models import Service, Project, Client, BlogPost, Review, Order, ServiceRequest, HeroBanner, Category, ContactInfo, Team, Career
|
||||
from .models import (
|
||||
Service, Project, Client, BlogPost, Review, Order, ServiceRequest,
|
||||
HeroBanner, Category, ContactInfo, Team, Career,
|
||||
ProjectMedia
|
||||
)
|
||||
from django.db.models import Avg
|
||||
from comunication.models import TelegramSettings
|
||||
import qrcode
|
||||
@@ -75,7 +79,45 @@ def service_detail(request, pk):
|
||||
|
||||
def project_detail(request, pk):
|
||||
project = get_object_or_404(Project, pk=pk)
|
||||
return render(request, 'web/project_detail.html', {'project': project})
|
||||
|
||||
# Увеличиваем счётчик просмотров
|
||||
project.views_count += 1
|
||||
project.save(update_fields=['views_count'])
|
||||
|
||||
# Получаем похожие проекты (той же категории или сервиса)
|
||||
similar_projects = Project.objects.filter(
|
||||
status='completed'
|
||||
).exclude(pk=project.pk)
|
||||
|
||||
if project.category:
|
||||
similar_projects = similar_projects.filter(category=project.category)
|
||||
elif project.service:
|
||||
similar_projects = similar_projects.filter(service=project.service)
|
||||
|
||||
similar_projects = similar_projects[:3]
|
||||
|
||||
return render(request, 'web/project_detail.html', {
|
||||
'project': project,
|
||||
'similar_projects': similar_projects
|
||||
})
|
||||
|
||||
def projects_list(request):
|
||||
"""Список всех завершённых проектов"""
|
||||
projects = Project.objects.filter(status='completed').order_by('-is_featured', '-display_order', '-completion_date')
|
||||
|
||||
# Фильтр по категории
|
||||
category_id = request.GET.get('category')
|
||||
if category_id:
|
||||
projects = projects.filter(categories__id=category_id)
|
||||
|
||||
# Получаем все категории с проектами
|
||||
categories = Category.objects.filter(projects__status='completed').distinct().order_by('order', 'name')
|
||||
|
||||
return render(request, 'web/projects_list.html', {
|
||||
'projects': projects,
|
||||
'categories': categories,
|
||||
'selected_category': category_id
|
||||
})
|
||||
|
||||
def client_detail(request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
@@ -108,9 +150,6 @@ def about_view(request):
|
||||
contact_info = ContactInfo.get_active()
|
||||
return render(request, 'web/about.html', {'contact_info': contact_info})
|
||||
|
||||
def portfolio_view(request):
|
||||
projects = Project.objects.all()
|
||||
return render(request, 'web/portfolio.html', {'projects': projects})
|
||||
|
||||
def blog_view(request):
|
||||
blog_posts = BlogPost.objects.all().order_by('-created_at')
|
||||
|
||||
Reference in New Issue
Block a user