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
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:
20
orders/migrations/0003_cartitem_selected_color.py
Normal file
20
orders/migrations/0003_cartitem_selected_color.py
Normal 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='Выбранный цвет'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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="Количество",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
color = Color.objects.get(pk=color_id)
|
||||||
|
except Color.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, created = add_item_to_cart(request.user, item)
|
_, 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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user