feat: enable color selection for cart items and enhance UI integration
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s

- Added `selected_color` field to `CartItem` model with migration
- Updated color swatch behavior on product detail page with selection logic and disabled button handling
- Improved cart templates to display selected color alongside item details
- Modified `add_item_to_cart` service and `add` view to handle color selection
- Enhanced styles for active swatches and cart item color display
This commit is contained in:
2026-05-26 03:12:00 +02:00
parent af129309ab
commit 56af404dc9
8 changed files with 79 additions and 5 deletions

View File

@@ -0,0 +1,20 @@
# Generated by Django 6.0.5 on 2026-05-25 22:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('items', '0005_remove_item_color_item_colors'),
('orders', '0002_order_orderitem'),
]
operations = [
migrations.AddField(
model_name='cartitem',
name='selected_color',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='items.color', verbose_name='Выбранный цвет'),
),
]

View File

@@ -36,6 +36,12 @@ class CartItem(models.Model):
related_name="cart_entries", related_name="cart_entries",
verbose_name="Товар", verbose_name="Товар",
) )
selected_color = models.ForeignKey(
'items.Color',
on_delete=models.SET_NULL,
null=True, blank=True,
verbose_name='Выбранный цвет',
)
quantity = models.PositiveIntegerField( quantity = models.PositiveIntegerField(
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
verbose_name="Количество", verbose_name="Количество",

View File

@@ -72,11 +72,13 @@ def get_user_cart_items(user: Any) -> list[CartItem]:
return [] return []
return list( return list(
cart.items.select_related("item__category", "item__subcategory").prefetch_related("item__images") cart.items
.select_related("item__category", "item__subcategory", "selected_color")
.prefetch_related("item__images")
) )
def add_item_to_cart(user: Any, item: Item, quantity: int = 1) -> tuple[CartItem, bool]: def add_item_to_cart(user: Any, item: Item, quantity: int = 1, color=None) -> tuple[CartItem, bool]:
if quantity < 1: if quantity < 1:
raise ValidationError("Количество товара должно быть не меньше 1.") raise ValidationError("Количество товара должно быть не меньше 1.")
@@ -87,8 +89,11 @@ def add_item_to_cart(user: Any, item: Item, quantity: int = 1) -> tuple[CartItem
cart_item, created = CartItem.objects.get_or_create( cart_item, created = CartItem.objects.get_or_create(
cart=cart, cart=cart,
item=item, item=item,
defaults={"quantity": quantity}, defaults={"quantity": quantity, "selected_color": color},
) )
if not created and cart_item.selected_color_id != (color.pk if color else None):
cart_item.selected_color = color
cart_item.save(update_fields=["selected_color", "updated_at"])
return cart_item, created return cart_item, created

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from items.models import Item from items.models import Color, Item
from orders.forms import CheckoutForm from orders.forms import CheckoutForm
from orders.models import CartItem, Order from orders.models import CartItem, Order
from orders.services import ( from orders.services import (
@@ -112,8 +112,16 @@ def success(request: HttpRequest, order_id: int) -> HttpResponse:
def add(request: HttpRequest) -> HttpResponse: def add(request: HttpRequest) -> HttpResponse:
item = get_object_or_404(Item, pk=request.POST.get("item_id")) item = get_object_or_404(Item, pk=request.POST.get("item_id"))
color_id = request.POST.get("color_id")
color = None
if color_id:
try: try:
_, created = add_item_to_cart(request.user, item) color = Color.objects.get(pk=color_id)
except Color.DoesNotExist:
pass
try:
_, created = add_item_to_cart(request.user, item, color=color)
if created: if created:
messages.success(request, f"Товар «{item.name}» добавлен в корзину.") messages.success(request, f"Товар «{item.name}» добавлен в корзину.")
else: else:

View File

@@ -67,6 +67,13 @@
line-height: 1.1; line-height: 1.1;
} }
.cart-item__color {
display: block;
color: #63768d;
font-size: 14px;
margin-top: 4px;
}
.cart-item__price { .cart-item__price {
margin-top: 24px; margin-top: 24px;
color: var(--dark); color: var(--dark);

View File

@@ -491,10 +491,14 @@ aside {
height: 46px; height: 46px;
border-radius: 50%; border-radius: 50%;
display: block; display: block;
border: 2px solid rgba(54, 33, 62, 0.18);
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.15s;
} }
.product-summary__swatch--active { .product-summary__swatch--active {
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #554971; box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #554971;
border-color: #554971;
} }
.product-summary__description { .product-summary__description {

View File

@@ -64,6 +64,7 @@
class="product-summary__swatch" class="product-summary__swatch"
style="background: {{ color.hex_code }};" style="background: {{ color.hex_code }};"
title="{{ color.name }}" title="{{ color.name }}"
data-color-id="{{ color.id }}"
></span> ></span>
{% endfor %} {% endfor %}
</div> </div>
@@ -84,6 +85,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}"> <input type="hidden" name="item_id" value="{{ item.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}"> <input type="hidden" name="next" value="{{ request.get_full_path }}">
<input type="hidden" name="color_id" id="selected-color-id" value="">
<button class="product-summary__button product-summary__button--primary" type="submit" {% if not item.is_available %}disabled{% endif %}> <button class="product-summary__button product-summary__button--primary" type="submit" {% if not item.is_available %}disabled{% endif %}>
В корзину В корзину
</button> </button>
@@ -109,6 +111,25 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
Fancybox.bind('[data-fancybox="product-gallery"]'); Fancybox.bind('[data-fancybox="product-gallery"]');
(function () {
var swatches = document.querySelectorAll('.product-summary__swatch[data-color-id]');
var colorInput = document.getElementById('selected-color-id');
var cartBtn = document.querySelector('.product-summary__button--primary[type="submit"]');
if (!swatches.length || !colorInput) { return; }
if (cartBtn) { cartBtn.disabled = true; }
swatches.forEach(function (swatch) {
swatch.addEventListener('click', function () {
swatches.forEach(function (s) { s.classList.remove('product-summary__swatch--active'); });
this.classList.add('product-summary__swatch--active');
colorInput.value = this.dataset.colorId || '';
if (cartBtn) { cartBtn.disabled = false; }
});
});
}());
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -34,6 +34,9 @@
<div class="cart-item__info"> <div class="cart-item__info">
<a class="cart-item__title" href="{% url 'detail' item.category.slug item.slug %}">{{ item.name }}</a> <a class="cart-item__title" href="{% url 'detail' item.category.slug item.slug %}">{{ item.name }}</a>
{% if cart_item.selected_color %}
<span class="cart-item__color">({{ cart_item.selected_color.name }})</span>
{% endif %}
<p class="cart-item__price">{{ item.price }} руб.</p> <p class="cart-item__price">{{ item.price }} руб.</p>
<div class="cart-item__controls"> <div class="cart-item__controls">