Добавлена система проектов с автоматическим ресайзом изображений и адаптивным дизайном
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:
2025-11-26 09:44:14 +09:00
parent 5bcf3e8198
commit e7d6d5262d
26 changed files with 3029 additions and 447 deletions

View File

@@ -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'],
},
),
]

View File

@@ -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',
),
]

View File

@@ -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='Проект'),
),
]