Make product cards fully clickable
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s

This commit is contained in:
2026-06-16 00:06:21 +02:00
parent a84b9af926
commit 1188cef7be
11 changed files with 133 additions and 9 deletions

View File

@@ -75,6 +75,16 @@ class FavoritePageTests(TestCase):
self.assertContains(response, "/media/items/sink-primary.jpg")
self.assertContains(response, 'href="/catalog/plumbing/kitchen-sink/"', html=False)
self.assertContains(response, 'href="/catalog/plumbing/mono-tap/"', html=False)
self.assertContains(
response,
'<a class="item-card-overlay" href="/catalog/plumbing/kitchen-sink/"',
html=False,
)
self.assertContains(
response,
'<a class="item-card-overlay" href="/catalog/plumbing/mono-tap/"',
html=False,
)
self.assertContains(response, "Удалить из избранного")
def test_toggle_endpoint_adds_and_removes_favorite(self):

View File

@@ -66,6 +66,11 @@ class HomePopularProductsTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, visible_item.name)
self.assertContains(response, reverse("detail", args=[self.category.slug, visible_item.slug]))
self.assertContains(
response,
f'<a class="product-card-link" href="{reverse("detail", args=[self.category.slug, visible_item.slug])}"',
html=False,
)
self.assertContains(response, "1600,00 руб.")
self.assertNotContains(response, "Hidden sink")

View File

@@ -279,6 +279,42 @@ class CatalogViewTests(TestCase):
response = self.client.get("/catalog/")
self.assertContains(response, 'href="/catalog/plumbing/kitchen-sink/"')
self.assertContains(
response,
'<a class="item-card-overlay" href="/catalog/plumbing/kitchen-sink/"',
html=False,
)
def test_sidebar_product_cards_include_full_card_links(self):
sale_item = Item.objects.create(
name="Акционная ванна",
slug="sale-bath",
category=self.plumbing,
price="900.00",
discount_price="750.00",
is_available=True,
)
new_item = Item.objects.create(
name="Новый душ",
slug="new-shower",
category=self.plumbing,
price="500.00",
is_available=True,
is_new=True,
)
response = self.client.get("/catalog/")
self.assertContains(
response,
f'<a class="sidebar-mini-card__overlay" href="/catalog/plumbing/{sale_item.slug}/"',
html=False,
)
self.assertContains(
response,
f'<a class="sidebar-mini-card__overlay" href="/catalog/plumbing/{new_item.slug}/"',
html=False,
)
def test_catalog_item_card_shows_favorite_button_for_authenticated_user(self):
user = get_user_model().objects.create_user(

View File

@@ -125,6 +125,13 @@ class CartFlowTests(TestCase):
self.assertContains(response, "3200,00 руб.")
self.assertContains(response, "3295,00 руб.")
self.assertContains(response, "/media/items/cart-item.jpg")
self.assertContains(
response,
'<a class="cart-item__overlay" href="/catalog/plumbing/kitchen-sink/"',
html=False,
)
self.assertContains(response, 'id="cart-increment-', html=False)
self.assertContains(response, 'action="/orders/cart/remove/"', html=False)
self.assertNotContains(response, 'name="promo_code"', html=False)
def test_cart_page_redirects_guest_to_login(self):

View File

@@ -26,13 +26,17 @@
}
.cart-item {
position: relative;
display: grid;
grid-template-columns: 230px minmax(0, 1fr);
gap: 34px;
align-items: start;
cursor: pointer;
}
.cart-item__media {
position: relative;
z-index: 1;
display: block;
width: 100%;
aspect-ratio: 1.55 / 1;
@@ -40,6 +44,13 @@
background: rgba(99, 118, 141, 0.12);
}
.cart-item__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: block;
}
.cart-item__media img {
display: block;
width: 100%;
@@ -61,6 +72,8 @@
}
.cart-item__title {
position: relative;
z-index: 1;
display: inline-block;
color: var(--dark);
font-size: 25px;
@@ -81,6 +94,8 @@
}
.cart-item__controls {
position: relative;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;

View File

@@ -200,9 +200,11 @@ aside {
}
.item-card {
position: relative;
min-width: 0;
border: 1px solid rgba(54, 33, 62, 0.14);
background: #ffffff;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
@@ -213,6 +215,15 @@ aside {
}
.item-card-media-link {
position: relative;
z-index: 1;
display: block;
}
.item-card-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: block;
}
@@ -259,6 +270,8 @@ aside {
}
.item-card-favorite-form {
position: relative;
z-index: 3;
flex: 0 0 auto;
}
@@ -296,10 +309,14 @@ aside {
}
.item-card-favorite-button--login {
position: relative;
z-index: 3;
text-decoration: none;
}
.item-card-title-link {
position: relative;
z-index: 1;
display: block;
}
@@ -688,13 +705,24 @@ aside {
/* Sidebar mini-cards */
.sidebar-mini-card {
position: relative;
display: flex;
gap: 10px;
align-items: flex-start;
margin-bottom: 12px;
cursor: pointer;
}
.sidebar-mini-card__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: block;
}
.sidebar-mini-card__img-link {
position: relative;
z-index: 1;
flex: 0 0 52px;
width: 52px;
height: 52px;
@@ -719,6 +747,8 @@ aside {
}
.sidebar-mini-card__name {
position: relative;
z-index: 1;
color: #36213e;
font-size: 15px;
line-height: 1.2;

View File

@@ -507,6 +507,7 @@
flex-direction: column;
background: #ffffff;
border: 1px solid rgba(54, 33, 62, 0.12);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
@@ -517,6 +518,8 @@
}
.product-media {
position: relative;
z-index: 1;
min-height: 0;
flex: 1 1 auto;
display: block;
@@ -524,6 +527,13 @@
background: rgba(99, 118, 141, 0.1);
}
.product-card-link {
position: absolute;
inset: 0;
z-index: 2;
display: block;
}
.product-media img {
display: block;
width: 100%;
@@ -545,7 +555,7 @@
.product-actions {
position: absolute;
z-index: 2;
z-index: 3;
top: 18px;
left: 18px;
display: flex;
@@ -604,6 +614,8 @@
}
.product-name {
position: relative;
z-index: 1;
min-height: 52px;
margin-top: 8px;
display: -webkit-box;

View File

@@ -1,5 +1,7 @@
{% url 'detail' item.category.slug item.slug as item_url %}
<article class="item-card{% if item.id in favorite_item_ids %} is-favorite{% endif %}" data-item-card="{{ item.id }}">
<a class="item-card-media-link" href="{% url 'detail' item.category.slug item.slug %}" aria-label="Открыть товар {{ item.name }}">
<a class="item-card-overlay" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}" tabindex="-1" aria-hidden="true"></a>
<a class="item-card-media-link" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}">
{% with primary_image=item.primary_image %}
<div class="item-card-image">
{% if primary_image %}
@@ -44,7 +46,7 @@
{% endif %}
</div>
<a class="item-card-title-link" href="{% url 'detail' item.category.slug item.slug %}" aria-label="Открыть товар {{ item.name }}">
<a class="item-card-title-link" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}">
<h2 class="item-card-title">{{ item.name }}</h2>
</a>
<div class="item-card-footer">

View File

@@ -104,15 +104,17 @@
</h2>
{% for sale_item in sidebar_sale_items %}
<div class="sidebar-mini-card">
{% url 'detail' sale_item.category.slug sale_item.slug as sale_item_url %}
<a class="sidebar-mini-card__overlay" href="{{ sale_item_url }}" aria-label="Открыть товар {{ sale_item.name }}" tabindex="-1" aria-hidden="true"></a>
{% 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">
<a href="{{ sale_item_url }}" 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>
<a href="{{ sale_item_url }}" class="sidebar-mini-card__name">{{ sale_item.name }}</a>
<span class="sidebar-mini-card__price">{{ sale_item.display_price }} руб.</span>
</div>
</div>
@@ -130,15 +132,17 @@
</h2>
{% for new_item in sidebar_new_items %}
<div class="sidebar-mini-card">
{% url 'detail' new_item.category.slug new_item.slug as new_item_url %}
<a class="sidebar-mini-card__overlay" href="{{ new_item_url }}" aria-label="Открыть товар {{ new_item.name }}" tabindex="-1" aria-hidden="true"></a>
{% 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">
<a href="{{ new_item_url }}" 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>
<a href="{{ new_item_url }}" class="sidebar-mini-card__name">{{ new_item.name }}</a>
<span class="sidebar-mini-card__price">{{ new_item.display_price }} руб.</span>
</div>
</div>

View File

@@ -7,6 +7,7 @@
{% for item in home_products %}
{% url 'detail' item.category.slug item.slug as item_url %}
<article class="product product-{{ forloop.counter }}">
<a class="product-card-link" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}" tabindex="-1" aria-hidden="true"></a>
<a class="product-media" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}">
{% with primary_image=item.primary_image %}
{% if primary_image %}

View File

@@ -25,8 +25,10 @@
<div class="cart-items">
{% for cart_item in cart_items %}
{% with item=cart_item.item %}
{% url 'detail' item.category.slug item.slug as item_url %}
<article class="cart-item" data-auth-cart-item data-current-quantity="{{ cart_item.quantity }}">
<a class="cart-item__media" href="{% url 'detail' item.category.slug item.slug %}">
<a class="cart-item__overlay" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}" tabindex="-1" aria-hidden="true"></a>
<a class="cart-item__media" href="{{ item_url }}">
{% with primary_image=item.primary_image %}
{% if primary_image %}
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
@@ -37,7 +39,7 @@
</a>
<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="{{ item_url }}">{{ item.name }}</a>
{% if cart_item.selected_color %}
<span class="cart-item__color">({{ cart_item.selected_color.name }})</span>
{% endif %}