feat: add color, discount price, and new item attributes to catalog and update UI
All checks were successful
Build and Push Docker Image / build (push) Successful in 25s
All checks were successful
Build and Push Docker Image / build (push) Successful in 25s
- Introduced `Color` model with admin and added a color field to items - Added `discount_price` and `is_new` fields to the `Item` model with necessary migrations - Updated pricing logic to display original and discount prices in templates - Enhanced product detail and item card templates with color swatches and metadata - Added sidebar sections for sales and new items with paginated mini-cards - Updated styles to support discount pricing, swatches, and sidebar mini-cards - Extended catalog views to filter by sales or new items and updated filtering logic
This commit is contained in:
@@ -3,7 +3,7 @@ from django.contrib import admin
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .models import Category, Item, ItemImage, Subcategory
|
from .models import Category, Color, Item, ItemImage, Subcategory
|
||||||
|
|
||||||
|
|
||||||
def get_subcategory_queryset(category_id):
|
def get_subcategory_queryset(category_id):
|
||||||
@@ -18,6 +18,11 @@ def get_subcategory_queryset(category_id):
|
|||||||
return Subcategory.objects.filter(category_id=category_id).order_by('name')
|
return Subcategory.objects.filter(category_id=category_id).order_by('name')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Color)
|
||||||
|
class ColorAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'hex_code')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'slug')
|
list_display = ('name', 'slug')
|
||||||
@@ -52,8 +57,9 @@ class ItemImageInline(admin.TabularInline):
|
|||||||
@admin.register(Item)
|
@admin.register(Item)
|
||||||
class ItemAdmin(admin.ModelAdmin):
|
class ItemAdmin(admin.ModelAdmin):
|
||||||
form = ItemAdminForm
|
form = ItemAdminForm
|
||||||
list_display = ('name', 'category', 'subcategory', 'price', 'is_available', 'created_at')
|
save_as = True
|
||||||
list_filter = ('is_available', 'category', 'subcategory', 'created_at')
|
list_display = ('name', 'category', 'subcategory', 'price', 'discount_price', 'is_new', 'is_available', 'created_at')
|
||||||
|
list_filter = ('is_available', 'is_new', 'color', 'category', 'subcategory', 'created_at')
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
inlines = (ItemImageInline,)
|
inlines = (ItemImageInline,)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-25 15:20
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('items', '0003_subcategory_item_subcategory_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Color',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Название цвета')),
|
||||||
|
('hex_code', models.CharField(help_text='Например, #FF5500', max_length=7, verbose_name='Цвет (HEX)')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Цвет',
|
||||||
|
'verbose_name_plural': 'Цвета',
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='discount_price',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена со скидкой'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='is_new',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Новинка'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='color',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items', to='items.color', verbose_name='Цвет'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class Color(models.Model):
|
||||||
|
name = models.CharField(max_length=100, verbose_name='Название цвета')
|
||||||
|
hex_code = models.CharField(max_length=7, verbose_name='Цвет (HEX)', help_text='Например, #FF5500')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
verbose_name = 'Цвет'
|
||||||
|
verbose_name_plural = 'Цвета'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name} ({self.hex_code})'
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
name = models.CharField(max_length=255, verbose_name='Название')
|
name = models.CharField(max_length=255, verbose_name='Название')
|
||||||
slug = models.SlugField(unique=True, verbose_name='URL')
|
slug = models.SlugField(unique=True, verbose_name='URL')
|
||||||
@@ -53,7 +68,20 @@ class Item(models.Model):
|
|||||||
)
|
)
|
||||||
description = models.TextField(blank=True, verbose_name='Описание')
|
description = models.TextField(blank=True, verbose_name='Описание')
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Цена')
|
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Цена')
|
||||||
|
discount_price = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2,
|
||||||
|
null=True, blank=True,
|
||||||
|
verbose_name='Цена со скидкой',
|
||||||
|
)
|
||||||
is_available = models.BooleanField(default=True, verbose_name='В наличии')
|
is_available = models.BooleanField(default=True, verbose_name='В наличии')
|
||||||
|
is_new = models.BooleanField(default=False, verbose_name='Новинка')
|
||||||
|
color = models.ForeignKey(
|
||||||
|
'Color',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True, blank=True,
|
||||||
|
related_name='items',
|
||||||
|
verbose_name='Цвет',
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='Дата обновления')
|
updated_at = models.DateTimeField(auto_now=True, verbose_name='Дата обновления')
|
||||||
|
|
||||||
@@ -76,6 +104,14 @@ class Item(models.Model):
|
|||||||
def primary_image(self):
|
def primary_image(self):
|
||||||
return next(iter(self.images.all()), None)
|
return next(iter(self.images.all()), None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_price(self) -> Decimal:
|
||||||
|
return self.discount_price if self.discount_price is not None else self.price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_discount(self) -> bool:
|
||||||
|
return self.discount_price is not None
|
||||||
|
|
||||||
|
|
||||||
class ItemImage(models.Model):
|
class ItemImage(models.Model):
|
||||||
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='images', verbose_name='Товар')
|
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='images', verbose_name='Товар')
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ def _build_catalog_url(
|
|||||||
search_query: str = "",
|
search_query: str = "",
|
||||||
min_price: str = "",
|
min_price: str = "",
|
||||||
max_price: str = "",
|
max_price: str = "",
|
||||||
|
sale: bool = False,
|
||||||
|
new: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
base_url = "/catalog/"
|
base_url = "/catalog/"
|
||||||
if category_slug:
|
if category_slug:
|
||||||
@@ -57,6 +59,10 @@ def _build_catalog_url(
|
|||||||
query_params["min_price"] = min_price
|
query_params["min_price"] = min_price
|
||||||
if max_price:
|
if max_price:
|
||||||
query_params["max_price"] = max_price
|
query_params["max_price"] = max_price
|
||||||
|
if sale:
|
||||||
|
query_params["sale"] = "1"
|
||||||
|
if new:
|
||||||
|
query_params["new"] = "1"
|
||||||
|
|
||||||
if not query_params:
|
if not query_params:
|
||||||
return base_url
|
return base_url
|
||||||
@@ -92,6 +98,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
min_price_raw = request.GET.get('min_price', '').strip()
|
min_price_raw = request.GET.get('min_price', '').strip()
|
||||||
max_price_raw = request.GET.get('max_price', '').strip()
|
max_price_raw = request.GET.get('max_price', '').strip()
|
||||||
selected_scope = request.GET.get('category', '').strip()
|
selected_scope = request.GET.get('category', '').strip()
|
||||||
|
filter_sale = request.GET.get('sale') == '1'
|
||||||
|
filter_new = request.GET.get('new') == '1'
|
||||||
|
|
||||||
if category is None and selected_scope:
|
if category is None and selected_scope:
|
||||||
category_slug, selected_subcategory_slug = _resolve_catalog_scope(selected_scope)
|
category_slug, selected_subcategory_slug = _resolve_catalog_scope(selected_scope)
|
||||||
@@ -103,6 +111,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
min_price=min_price_raw,
|
min_price=min_price_raw,
|
||||||
max_price=max_price_raw,
|
max_price=max_price_raw,
|
||||||
|
sale=filter_sale,
|
||||||
|
new=filter_new,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,17 +153,23 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
if normalized_search_query in item.name.casefold()
|
if normalized_search_query in item.name.casefold()
|
||||||
]
|
]
|
||||||
|
|
||||||
item_prices = [item.price for item in items]
|
if filter_sale:
|
||||||
|
items = [item for item in items if item.discount_price is not None]
|
||||||
|
|
||||||
|
if filter_new:
|
||||||
|
items = [item for item in items if item.is_new]
|
||||||
|
|
||||||
|
item_prices = [item.display_price for item in items]
|
||||||
slider_min_bound = min(item_prices, default=Decimal('0'))
|
slider_min_bound = min(item_prices, default=Decimal('0'))
|
||||||
slider_max_bound = max(item_prices, default=slider_min_bound)
|
slider_max_bound = max(item_prices, default=slider_min_bound)
|
||||||
|
|
||||||
min_price = _parse_price(min_price_raw)
|
min_price = _parse_price(min_price_raw)
|
||||||
if min_price is not None:
|
if min_price is not None:
|
||||||
items = [item for item in items if item.price >= min_price]
|
items = [item for item in items if item.display_price >= min_price]
|
||||||
|
|
||||||
max_price = _parse_price(max_price_raw)
|
max_price = _parse_price(max_price_raw)
|
||||||
if max_price is not None:
|
if max_price is not None:
|
||||||
items = [item for item in items if item.price <= max_price]
|
items = [item for item in items if item.display_price <= max_price]
|
||||||
|
|
||||||
slider_selected_min = min_price if min_price is not None else slider_min_bound
|
slider_selected_min = min_price if min_price is not None else slider_min_bound
|
||||||
slider_selected_max = max_price if max_price is not None else slider_max_bound
|
slider_selected_max = max_price if max_price is not None else slider_max_bound
|
||||||
@@ -165,6 +181,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
min_price=min_price_raw,
|
min_price=min_price_raw,
|
||||||
max_price=max_price_raw,
|
max_price=max_price_raw,
|
||||||
|
sale=filter_sale,
|
||||||
|
new=filter_new,
|
||||||
)
|
)
|
||||||
for catalog_category in categories:
|
for catalog_category in categories:
|
||||||
catalog_category.catalog_url = _build_catalog_url(
|
catalog_category.catalog_url = _build_catalog_url(
|
||||||
@@ -172,6 +190,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
min_price=min_price_raw,
|
min_price=min_price_raw,
|
||||||
max_price=max_price_raw,
|
max_price=max_price_raw,
|
||||||
|
sale=filter_sale,
|
||||||
|
new=filter_new,
|
||||||
)
|
)
|
||||||
|
|
||||||
subcategories = list(subcategories)
|
subcategories = list(subcategories)
|
||||||
@@ -182,6 +202,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
min_price=min_price_raw,
|
min_price=min_price_raw,
|
||||||
max_price=max_price_raw,
|
max_price=max_price_raw,
|
||||||
|
sale=filter_sale,
|
||||||
|
new=filter_new,
|
||||||
)
|
)
|
||||||
for catalog_subcategory in subcategories:
|
for catalog_subcategory in subcategories:
|
||||||
catalog_subcategory.catalog_url = _build_catalog_url(
|
catalog_subcategory.catalog_url = _build_catalog_url(
|
||||||
@@ -190,10 +212,25 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
search_query=search_query,
|
search_query=search_query,
|
||||||
min_price=min_price_raw,
|
min_price=min_price_raw,
|
||||||
max_price=max_price_raw,
|
max_price=max_price_raw,
|
||||||
|
sale=filter_sale,
|
||||||
|
new=filter_new,
|
||||||
)
|
)
|
||||||
|
|
||||||
price_filter_action = _build_catalog_url(selected_category.id)
|
price_filter_action = _build_catalog_url(selected_category.id)
|
||||||
|
|
||||||
|
sidebar_sale_items = (
|
||||||
|
Item.objects.filter(discount_price__isnull=False, is_available=True)
|
||||||
|
.prefetch_related('images')
|
||||||
|
.order_by('-created_at')[:3]
|
||||||
|
)
|
||||||
|
sidebar_new_items = (
|
||||||
|
Item.objects.filter(is_new=True, is_available=True)
|
||||||
|
.prefetch_related('images')
|
||||||
|
.order_by('-created_at')[:3]
|
||||||
|
)
|
||||||
|
sale_all_url = _build_catalog_url(sale=True)
|
||||||
|
new_all_url = _build_catalog_url(new=True)
|
||||||
|
|
||||||
return render(request, 'catalog/index.html', {
|
return render(request, 'catalog/index.html', {
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'subcategories': subcategories,
|
'subcategories': subcategories,
|
||||||
@@ -211,13 +248,19 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
|||||||
'price_slider_selected_min': _format_price_value(slider_selected_min),
|
'price_slider_selected_min': _format_price_value(slider_selected_min),
|
||||||
'price_slider_selected_max': _format_price_value(slider_selected_max),
|
'price_slider_selected_max': _format_price_value(slider_selected_max),
|
||||||
'favorite_item_ids': get_user_favorite_item_ids(request.user),
|
'favorite_item_ids': get_user_favorite_item_ids(request.user),
|
||||||
|
'filter_sale': filter_sale,
|
||||||
|
'filter_new': filter_new,
|
||||||
|
'sidebar_sale_items': sidebar_sale_items,
|
||||||
|
'sidebar_new_items': sidebar_new_items,
|
||||||
|
'sale_all_url': sale_all_url,
|
||||||
|
'new_all_url': new_all_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def detail(request: HttpRequest, category: str, slug: str) -> HttpResponse:
|
def detail(request: HttpRequest, category: str, slug: str) -> HttpResponse:
|
||||||
try:
|
try:
|
||||||
item = (
|
item = (
|
||||||
Item.objects.select_related('category', 'subcategory')
|
Item.objects.select_related('category', 'subcategory', 'color')
|
||||||
.prefetch_related('images')
|
.prefetch_related('images')
|
||||||
.get(category__slug=category.lower(), slug=slug)
|
.get(category__slug=category.lower(), slug=slug)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -298,6 +298,12 @@ aside {
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-card-price-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.item-card-price {
|
.item-card-price {
|
||||||
color: #36213e;
|
color: #36213e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -305,6 +311,18 @@ aside {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-card-price--discount {
|
||||||
|
color: #c54c5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-price-original {
|
||||||
|
color: #8a8a9a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: line-through;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.item-card-availability {
|
.item-card-availability {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: #554971;
|
color: #554971;
|
||||||
@@ -411,12 +429,18 @@ aside {
|
|||||||
|
|
||||||
.product-summary__meta {
|
.product-summary__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-summary__price-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.product-summary__price {
|
.product-summary__price {
|
||||||
color: #36213e;
|
color: #36213e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -424,6 +448,23 @@ aside {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-summary__price--discount {
|
||||||
|
color: #c54c5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-summary__price-original {
|
||||||
|
color: #8a8a9a;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: line-through;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-summary__color-name {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #554971;
|
||||||
|
}
|
||||||
|
|
||||||
.product-summary__availability {
|
.product-summary__availability {
|
||||||
color: #554971;
|
color: #554971;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
@@ -452,20 +493,8 @@ aside {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-summary__swatch--dark {
|
.product-summary__swatch--active {
|
||||||
background: #303438;
|
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #554971;
|
||||||
}
|
|
||||||
|
|
||||||
.product-summary__swatch--graphite {
|
|
||||||
background: #5f6767;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-summary__swatch--taupe {
|
|
||||||
background: #927d68;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-summary__swatch--sand {
|
|
||||||
background: #cfc3ae;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-summary__description {
|
.product-summary__description {
|
||||||
@@ -606,3 +635,83 @@ aside {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar section links (Акции / Новинки headings) */
|
||||||
|
.sidebar-section-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section-link:hover,
|
||||||
|
.sidebar-section-link.active {
|
||||||
|
color: #36213e;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar mini-cards */
|
||||||
|
.sidebar-mini-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__img-link {
|
||||||
|
flex: 0 0 52px;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(99, 118, 141, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__img-link img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__body {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__name {
|
||||||
|
color: #36213e;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: text-decoration-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__name:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mini-card__price {
|
||||||
|
color: #554971;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-more-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #63768d;
|
||||||
|
font-size: 15px;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-more-link:hover {
|
||||||
|
color: #36213e;
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,14 @@
|
|||||||
<h2 class="item-card-title">{{ item.name }}</h2>
|
<h2 class="item-card-title">{{ item.name }}</h2>
|
||||||
</a>
|
</a>
|
||||||
<div class="item-card-footer">
|
<div class="item-card-footer">
|
||||||
<span class="item-card-price">{{ item.price }} руб.</span>
|
<div class="item-card-price-block">
|
||||||
|
<span class="item-card-price{% if item.has_discount %} item-card-price--discount{% endif %}">
|
||||||
|
{{ item.display_price }} руб.
|
||||||
|
</span>
|
||||||
|
{% if item.has_discount %}
|
||||||
|
<span class="item-card-price-original">{{ item.price }} руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="item-card-availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
|
<span class="item-card-availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
|
||||||
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
|
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,6 +34,12 @@
|
|||||||
{% if selected_subcategory.id != None %}
|
{% if selected_subcategory.id != None %}
|
||||||
<input type="hidden" name="subcategory" value="{{ selected_subcategory.id }}">
|
<input type="hidden" name="subcategory" value="{{ selected_subcategory.id }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if filter_sale %}
|
||||||
|
<input type="hidden" name="sale" value="1">
|
||||||
|
{% endif %}
|
||||||
|
{% if filter_new %}
|
||||||
|
<input type="hidden" name="new" value="1">
|
||||||
|
{% endif %}
|
||||||
<section class="a-section cost-section">
|
<section class="a-section cost-section">
|
||||||
<h2>Стоимость</h2>
|
<h2>Стоимость</h2>
|
||||||
<div
|
<div
|
||||||
@@ -86,13 +92,57 @@
|
|||||||
<section class="a-section material-section">
|
<section class="a-section material-section">
|
||||||
<h2>Материал</h2>
|
<h2>Материал</h2>
|
||||||
</section>
|
</section>
|
||||||
|
</form>
|
||||||
|
|
||||||
<section class="a-section promotions-section">
|
<section class="a-section promotions-section">
|
||||||
<h2>Акции</h2>
|
<h2>
|
||||||
|
<a href="{{ sale_all_url }}" class="sidebar-section-link{% if filter_sale %} active{% endif %}">Акции</a>
|
||||||
|
</h2>
|
||||||
|
{% for sale_item in sidebar_sale_items %}
|
||||||
|
<div class="sidebar-mini-card">
|
||||||
|
{% with img=sale_item.primary_image %}
|
||||||
|
{% if img %}
|
||||||
|
<a href="{% url 'detail' sale_item.category.slug sale_item.slug %}" class="sidebar-mini-card__img-link">
|
||||||
|
<img src="{{ img.image.url }}" alt="{{ img.alt|default:sale_item.name }}">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="sidebar-mini-card__body">
|
||||||
|
<a href="{% url 'detail' sale_item.category.slug sale_item.slug %}" class="sidebar-mini-card__name">{{ sale_item.name }}</a>
|
||||||
|
<span class="sidebar-mini-card__price">{{ sale_item.display_price }} руб.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="categories-empty">Акций пока нет.</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% if sidebar_sale_items %}
|
||||||
|
<a href="{{ sale_all_url }}" class="sidebar-more-link">Ещё</a>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="a-section new-section">
|
<section class="a-section new-section">
|
||||||
<h2>Новинки</h2>
|
<h2>
|
||||||
|
<a href="{{ new_all_url }}" class="sidebar-section-link{% if filter_new %} active{% endif %}">Новинки</a>
|
||||||
|
</h2>
|
||||||
|
{% for new_item in sidebar_new_items %}
|
||||||
|
<div class="sidebar-mini-card">
|
||||||
|
{% with img=new_item.primary_image %}
|
||||||
|
{% if img %}
|
||||||
|
<a href="{% url 'detail' new_item.category.slug new_item.slug %}" class="sidebar-mini-card__img-link">
|
||||||
|
<img src="{{ img.image.url }}" alt="{{ img.alt|default:new_item.name }}">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="sidebar-mini-card__body">
|
||||||
|
<a href="{% url 'detail' new_item.category.slug new_item.slug %}" class="sidebar-mini-card__name">{{ new_item.name }}</a>
|
||||||
|
<span class="sidebar-mini-card__price">{{ new_item.display_price }} руб.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="categories-empty">Новинок пока нет.</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% if sidebar_new_items %}
|
||||||
|
<a href="{{ new_all_url }}" class="sidebar-more-link">Ещё</a>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
</form>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -43,18 +43,29 @@
|
|||||||
<h1 class="product-summary__title">{{ item.name }}</h1>
|
<h1 class="product-summary__title">{{ item.name }}</h1>
|
||||||
|
|
||||||
<div class="product-summary__meta">
|
<div class="product-summary__meta">
|
||||||
<span class="product-summary__price">{{ item.price }} руб.</span>
|
<div class="product-summary__price-block">
|
||||||
|
<span class="product-summary__price{% if item.has_discount %} product-summary__price--discount{% endif %}">
|
||||||
|
{{ item.display_price }} руб.
|
||||||
|
</span>
|
||||||
|
{% if item.has_discount %}
|
||||||
|
<span class="product-summary__price-original">{{ item.price }} руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="product-summary__availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
|
<span class="product-summary__availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
|
||||||
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
|
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-summary__swatches" aria-hidden="true">
|
{% if item.color %}
|
||||||
<span class="product-summary__swatch product-summary__swatch--dark"></span>
|
<div class="product-summary__swatches" aria-label="Цвет товара">
|
||||||
<span class="product-summary__swatch product-summary__swatch--graphite"></span>
|
<span
|
||||||
<span class="product-summary__swatch product-summary__swatch--taupe"></span>
|
class="product-summary__swatch product-summary__swatch--active"
|
||||||
<span class="product-summary__swatch product-summary__swatch--sand"></span>
|
style="background: {{ item.color.hex_code }};"
|
||||||
|
title="{{ item.color.name }}"
|
||||||
|
></span>
|
||||||
|
<span class="product-summary__color-name">{{ item.color.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if item.description %}
|
{% if item.description %}
|
||||||
<div class="product-summary__description">{{ item.description|linebreaksbr }}</div>
|
<div class="product-summary__description">{{ item.description|linebreaksbr }}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user