feat: add item images, product detail page, and gallery support in catalog
All checks were successful
Build and Push Docker Image / build (push) Successful in 45s

- Added `ItemImage` model with migration for image handling and metadata
- Implemented product gallery in `detail.html` with thumbnails for additional images
- Updated `Item` model to include `primary_image` property for gallery logic
- Enhanced `catalog/index.html` to support item thumbnails
- Updated `catalog.css` for responsive gallery and item list styling
- Extended catalog views to prefetch images for efficient queries
- Added comprehensive tests for catalog and detail views
- Configured Django to serve media files in development
- Updated admin with inline support for managing `ItemImage` entities
This commit is contained in:
2026-04-26 21:55:32 +02:00
parent d0651274d6
commit bb321dfa22
15 changed files with 817 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Category, Item from .models import Category, Item, ItemImage
@admin.register(Category) @admin.register(Category)
@@ -9,8 +9,15 @@ class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)} prepopulated_fields = {'slug': ('name',)}
class ItemImageInline(admin.TabularInline):
model = ItemImage
extra = 1
fields = ('image', 'alt', 'sort_order', 'is_primary')
@admin.register(Item) @admin.register(Item)
class ItemAdmin(admin.ModelAdmin): class ItemAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'is_available', 'created_at') list_display = ('name', 'category', 'price', 'is_available', 'created_at')
list_filter = ('is_available', 'category', 'created_at') list_filter = ('is_available', 'category', 'created_at')
prepopulated_fields = {'slug': ('name',)} prepopulated_fields = {'slug': ('name',)}
inlines = (ItemImageInline,)

View File

@@ -0,0 +1,30 @@
# Generated by Django 6.0.4 on 2026-04-26 14:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('items', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ItemImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='items/', verbose_name='Изображение')),
('alt', models.CharField(blank=True, max_length=255, verbose_name='Alt-текст')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
('is_primary', models.BooleanField(default=False, verbose_name='Главное изображение')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='items.item', verbose_name='Товар')),
],
options={
'verbose_name': 'Изображение товара',
'verbose_name_plural': 'Изображения товара',
'ordering': ('-is_primary', 'sort_order', 'id'),
},
),
]

View File

@@ -29,3 +29,23 @@ class Item(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@property
def primary_image(self):
return next(iter(self.images.all()), None)
class ItemImage(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='images', verbose_name='Товар')
image = models.ImageField(upload_to='items/', verbose_name='Изображение')
alt = models.CharField(max_length=255, blank=True, verbose_name='Alt-текст')
sort_order = models.PositiveIntegerField(default=0, verbose_name='Порядок')
is_primary = models.BooleanField(default=False, verbose_name='Главное изображение')
class Meta:
ordering = ('-is_primary', 'sort_order', 'id')
verbose_name = 'Изображение товара'
verbose_name_plural = 'Изображения товара'
def __str__(self):
return self.alt or f'Изображение товара {self.item.name}'

View File

@@ -1,3 +1,142 @@
from django.test import TestCase from django.test import TestCase, override_settings
# Create your tests here. from items.models import Category, Item, ItemImage
@override_settings(MEDIA_URL="/media/")
class CatalogViewTests(TestCase):
def setUp(self):
self.plumbing = Category.objects.create(name="Сантехника", slug="plumbing")
self.decor = Category.objects.create(name="Декор", slug="decor")
self.sink = Item.objects.create(
name="Кухонная мойка",
slug="kitchen-sink",
category=self.plumbing,
description="Искусственный камень, врезная на столешницу",
price="1600.00",
is_available=True,
)
self.mirror = Item.objects.create(
name="Зеркало Terra",
slug="terra-mirror",
category=self.decor,
description="Холодная подсветка, фронтальная",
price="200.00",
is_available=False,
)
def test_catalog_page_shows_items_from_all_categories(self):
response = self.client.get("/catalog/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Кухонная мойка")
self.assertContains(response, "Зеркало Terra")
def test_category_page_shows_only_selected_category_items(self):
response = self.client.get("/catalog/plumbing/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Кухонная мойка")
self.assertNotContains(response, "Зеркало Terra")
def test_unknown_category_returns_404(self):
response = self.client.get("/catalog/unknown/")
self.assertEqual(response.status_code, 404)
def test_sidebar_shows_all_categories_and_all_items_link(self):
response = self.client.get("/catalog/")
self.assertContains(response, "Все товары")
self.assertContains(response, "Сантехника")
self.assertContains(response, "Декор")
def test_item_card_uses_primary_image(self):
ItemImage.objects.create(
item=self.sink,
image="items/secondary.jpg",
sort_order=0,
is_primary=False,
)
ItemImage.objects.create(
item=self.sink,
image="items/primary.jpg",
sort_order=10,
is_primary=True,
)
response = self.client.get("/catalog/")
self.assertContains(response, "/media/items/primary.jpg")
self.assertNotContains(response, "/media/items/secondary.jpg")
def test_item_card_uses_first_ordered_image_without_primary(self):
ItemImage.objects.create(
item=self.sink,
image="items/second.jpg",
sort_order=20,
is_primary=False,
)
ItemImage.objects.create(
item=self.sink,
image="items/first.jpg",
sort_order=10,
is_primary=False,
)
response = self.client.get("/catalog/")
self.assertContains(response, "/media/items/first.jpg")
self.assertNotContains(response, "/media/items/second.jpg")
def test_catalog_item_card_links_to_detail_page(self):
response = self.client.get("/catalog/")
self.assertContains(response, 'href="/catalog/plumbing/kitchen-sink/"')
def test_detail_page_shows_item_data(self):
ItemImage.objects.create(
item=self.sink,
image="items/sink-primary.jpg",
alt="Главное фото мойки",
sort_order=0,
is_primary=True,
)
response = self.client.get("/catalog/plumbing/kitchen-sink/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Кухонная мойка")
self.assertContains(response, "Искусственный камень")
self.assertContains(response, "1600,00 руб.")
self.assertContains(response, "В наличии")
self.assertContains(response, "/media/items/sink-primary.jpg")
self.assertContains(response, "Главное фото мойки")
def test_detail_page_uses_primary_image_first(self):
ItemImage.objects.create(
item=self.sink,
image="items/secondary.jpg",
sort_order=0,
is_primary=False,
)
ItemImage.objects.create(
item=self.sink,
image="items/primary.jpg",
sort_order=10,
is_primary=True,
)
response = self.client.get("/catalog/plumbing/kitchen-sink/")
self.assertContains(response, '<img src="/media/items/primary.jpg"', html=False)
def test_detail_page_unknown_item_returns_404(self):
response = self.client.get("/catalog/plumbing/unknown/")
self.assertEqual(response.status_code, 404)
def test_detail_page_wrong_category_returns_404(self):
response = self.client.get("/catalog/decor/kitchen-sink/")
self.assertEqual(response.status_code, 404)

View File

@@ -4,5 +4,6 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.index), path('', views.index),
path('<str:category>/<slug:slug>/', views.detail, name='detail'),
path('<str:category>/', views.index) path('<str:category>/', views.index)
] ]

View File

@@ -1,13 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from django.http import HttpRequest, HttpResponse, Http404 from django.http import HttpRequest, HttpResponse, Http404
from django.shortcuts import render, redirect from django.shortcuts import render
from items.models import Category, Item from items.models import Category, Item
# Create your views here.
@dataclass @dataclass
class SelectedCategory: class SelectedCategory:
name: str | None name: str | None
@@ -16,15 +14,38 @@ class SelectedCategory:
def index(request: HttpRequest, category: str | None = None) -> HttpResponse: def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
selected_category = SelectedCategory(name=None, id=None) selected_category = SelectedCategory(name=None, id=None)
items = Item.objects.select_related('category').prefetch_related('images').order_by('category__name', 'name')
if category is not None: if category is not None:
try: try:
cat = Category.objects.get(slug=category.lower()) cat = Category.objects.get(slug=category.lower())
selected_category = SelectedCategory(name=cat.name, id=cat.slug) selected_category = SelectedCategory(name=cat.name, id=cat.slug)
except Category.DoesNotExist: except Category.DoesNotExist:
raise Http404() raise Http404()
items = items.filter(category=cat)
return render(request, 'catalog/index.html', { return render(request, 'catalog/index.html', {
'categories': Category.objects.all(), 'categories': Category.objects.order_by('name'),
'items': Item.objects.all(), 'items': items,
'selected_category': selected_category 'selected_category': selected_category
}) })
def detail(request: HttpRequest, category: str, slug: str) -> HttpResponse:
try:
item = (
Item.objects.select_related('category')
.prefetch_related('images')
.get(category__slug=category.lower(), slug=slug)
)
except Item.DoesNotExist:
raise Http404()
images = list(item.images.all())
return render(request, 'catalog/detail.html', {
'categories': Category.objects.order_by('name'),
'item': item,
'images': images,
'primary_image': images[0] if images else None,
})

View File

@@ -1,3 +1,4 @@
django django
Pillow
uvicorn uvicorn
whitenoise whitenoise

View File

@@ -272,6 +272,8 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.2/howto/static-files/ # https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Additional locations of static files # Additional locations of static files
STATICFILES_DIRS = [ STATICFILES_DIRS = [

View File

@@ -14,6 +14,8 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
@@ -27,3 +29,6 @@ urlpatterns = [
path('users/', include('users.urls')), path('users/', include('users.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,10 +1,426 @@
aside { aside {
width: 250px; flex: 0 0 250px;
} }
.catalog-container { .catalog-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; align-items: flex-start;
gap: 20px; gap: 28px;
margin-bottom: 80px;
}
.a-section {
margin-bottom: 24px;
}
.a-section h2 {
font-weight: 400;
font-size: 26px;
line-height: 1.2;
color: #36213e;
margin-bottom: 14px;
}
.categories {
display: flex;
flex-direction: column;
gap: 6px;
}
.category {
display: block;
padding: 6px 0;
color: #554971;
font-size: 18px;
line-height: 1.25;
transition: color 0.2s, text-decoration-color 0.2s;
}
.category:hover,
.category.active {
color: #36213e;
text-decoration: underline;
}
.categories-empty,
.item-list-empty {
color: #554971;
font-size: 18px;
line-height: 1.4;
}
.item-list {
flex: 1 1 auto;
min-width: 0;
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
}
.item-card {
min-width: 0;
border: 1px solid rgba(54, 33, 62, 0.14);
background: #ffffff;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.item-card:hover {
border-color: rgba(54, 33, 62, 0.3);
box-shadow: 0 18px 36px rgba(54, 33, 62, 0.1);
transform: translateY(-2px);
}
.item-card-link {
display: block;
height: 100%;
}
.item-card-image {
aspect-ratio: 4 / 3;
background: rgba(99, 118, 141, 0.1);
overflow: hidden;
}
.item-card-image img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.item-card-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #63768d;
font-size: 18px;
}
.item-card-body {
padding: 18px;
}
.item-card-category {
color: #63768d;
font-size: 15px;
line-height: 1.2;
margin-bottom: 8px;
}
.item-card-title {
color: #36213e;
font-weight: 400;
font-size: 24px;
line-height: 1.2;
display: -webkit-box;
min-height: 58px;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.item-card-description {
margin-top: 10px;
color: #554971;
font-size: 16px;
line-height: 1.35;
}
.item-card-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin-top: 18px;
}
.item-card-price {
color: #36213e;
font-weight: 700;
font-size: 22px;
line-height: 1.2;
}
.item-card-availability {
flex: 0 0 auto;
color: #554971;
font-size: 15px;
line-height: 1.2;
}
.item-card-availability.available {
color: #4f7d4f;
}
.item-card-availability.unavailable {
color: #a14b4b;
}
.product-detail {
display: grid;
grid-template-columns: minmax(300px, 420px) minmax(320px, 1fr);
gap: 78px;
align-items: start;
margin-top: 44px;
margin-bottom: 88px;
}
.product-gallery {
min-width: 0;
}
.product-gallery__main {
width: 100%;
aspect-ratio: 1 / 0.96;
background: rgba(99, 118, 141, 0.1);
overflow: hidden;
}
.product-gallery__main a,
.product-gallery__main img {
display: block;
width: 100%;
height: 100%;
}
.product-gallery__main img {
object-fit: cover;
}
.product-gallery__placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #63768d;
font-size: 20px;
}
.product-gallery__thumbs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 28px;
margin-top: 16px;
}
.product-gallery__thumb {
display: block;
aspect-ratio: 1.2 / 1;
background: rgba(99, 118, 141, 0.08);
border: 1px solid transparent;
overflow: hidden;
transition: border-color 0.2s, opacity 0.2s;
}
.product-gallery__thumb:hover,
.product-gallery__thumb.active {
border-color: rgba(54, 33, 62, 0.35);
}
.product-gallery__thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.product-summary {
min-width: 0;
padding-top: 4px;
}
.product-summary__category {
color: #63768d;
font-size: 16px;
line-height: 1.2;
margin-bottom: 10px;
}
.product-summary__title {
max-width: 650px;
color: #36213e;
font-weight: 400;
font-size: 42px;
line-height: 1.08;
}
.product-summary__meta {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 18px;
margin-top: 16px;
}
.product-summary__price {
color: #36213e;
font-weight: 700;
font-size: 28px;
line-height: 1.2;
}
.product-summary__availability {
color: #554971;
font-size: 17px;
line-height: 1.2;
}
.product-summary__availability.available {
color: #4f7d4f;
}
.product-summary__availability.unavailable {
color: #a14b4b;
}
.product-summary__swatches {
display: flex;
align-items: center;
gap: 14px;
margin-top: 24px;
}
.product-summary__swatch {
width: 46px;
height: 46px;
border-radius: 50%;
display: block;
}
.product-summary__swatch--dark {
background: #303438;
}
.product-summary__swatch--graphite {
background: #5f6767;
}
.product-summary__swatch--taupe {
background: #927d68;
}
.product-summary__swatch--sand {
background: #cfc3ae;
}
.product-summary__description {
max-width: 560px;
margin-top: 28px;
color: #36213e;
font-size: 18px;
line-height: 1.22;
}
.product-summary__actions {
display: grid;
grid-template-columns: minmax(160px, 245px) minmax(160px, 245px);
gap: 34px;
margin-top: 88px;
}
.product-summary__button {
min-height: 38px;
border: 1px solid #63768d;
font-family: 'Noah', sans-serif;
font-size: 18px;
line-height: 1;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.product-summary__button--primary {
background: #63768d;
color: #ffffff;
}
.product-summary__button--primary:hover {
background: #554971;
border-color: #554971;
}
.product-summary__button--secondary {
background: #ffffff;
color: #63768d;
}
.product-summary__button--secondary:hover {
border-color: #554971;
color: #36213e;
}
@media (max-width: 900px) {
.catalog-container {
flex-direction: column;
padding-left: 24px;
padding-right: 24px;
}
aside,
.item-list {
width: 100%;
flex-basis: auto;
}
.product-detail {
grid-template-columns: 1fr;
gap: 36px;
padding-left: 24px;
padding-right: 24px;
margin-top: 28px;
margin-bottom: 64px;
}
.product-summary__title {
font-size: 34px;
}
.product-summary__actions {
margin-top: 42px;
}
}
@media (max-width: 520px) {
.items-grid {
grid-template-columns: 1fr;
}
.item-card-footer {
align-items: flex-start;
flex-direction: column;
}
.product-gallery__thumbs {
gap: 12px;
}
.product-summary__title {
font-size: 30px;
}
.product-summary__swatch {
width: 40px;
height: 40px;
}
.product-summary__description {
font-size: 17px;
}
.product-summary__actions {
grid-template-columns: 1fr;
gap: 14px;
}
.product-summary__button {
min-height: 44px;
width: 100%;
}
} }

View File

@@ -437,3 +437,59 @@ footer {
padding: 40px 24px; padding: 40px 24px;
} }
} }
@media (max-width: 700px) {
.header-top {
flex-wrap: wrap;
gap: 12px 16px;
}
.logo {
font-size: 30px;
}
.header-top form {
order: 3;
width: 100%;
}
.search-wrap {
width: 100%;
max-width: none;
margin: 0;
}
.search-wrap select {
flex: 0 0 132px;
max-width: none;
padding-left: 8px;
padding-right: 8px;
}
.header-icons {
margin-left: auto;
gap: 14px;
}
.header-icons svg {
width: 24px;
height: 24px;
}
.nav-bar {
justify-content: flex-start;
gap: 24px;
overflow-x: auto;
scrollbar-width: thin;
}
.nav-links {
flex: 0 0 auto;
gap: 22px;
}
.nav-block {
width: auto;
flex: 0 0 auto;
}
}

View File

@@ -1,6 +1,32 @@
<main class="item-list"> <main class="item-list">
<div class="items-grid">
{% for item in items %} {% for item in items %}
<h1>item</h1> <article class="item-card">
{% endfor %} <a class="item-card-link" href="{% url 'detail' item.category.slug item.slug %}" aria-label="Открыть товар {{ item.name }}">
{% with primary_image=item.primary_image %}
<div class="item-card-image">
{% if primary_image %}
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
{% else %}
<div class="item-card-placeholder">Нет фото</div>
{% endif %}
</div>
{% endwith %}
<div class="item-card-body">
<div class="item-card-category">{{ item.category.name }}</div>
<h2 class="item-card-title">{{ item.name }}</h2>
<div class="item-card-footer">
<span class="item-card-price">{{ item.price }} руб.</span>
<span class="item-card-availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
</span>
</div>
</div>
</a>
</article>
{% empty %}
<p class="item-list-empty">В этой категории пока нет товаров.</p>
{% endfor %}
</div>
</main> </main>

View File

@@ -1,8 +1,13 @@
<aside> <aside>
<section class="categories a-section"> <section class="categories a-section">
<h2>Категории</h2> <h2>Категории</h2>
<a href="/catalog/" class="category {% if selected_category.id == None %}active{% endif %}">Все товары</a>
{% for category in categories %} {% for category in categories %}
<a href="/catalog/{{ category.slug }}" class="category">{{ category.name }}</a> <a href="/catalog/{{ category.slug }}/" class="category {% if selected_category.id == category.slug %}active{% endif %}">
{{ category.name }}
</a>
{% empty %}
<p class="categories-empty">Категории пока не добавлены.</p>
{% endfor %} {% endfor %}
</section> </section>
@@ -13,22 +18,18 @@
<form action=""> <form action="">
<section class="a-section cost-section"> <section class="a-section cost-section">
<h2>Стоимость</h2> <h2>Стоимость</h2>
</section> </section>
<section class="a-section color-section"> <section class="a-section color-section">
<h2>Цвет</h2> <h2>Цвет</h2>
</section> </section>
<section class="a-section material-section"> <section class="a-section material-section">
<h2>Материал</h2> <h2>Материал</h2>
</section> </section>
<section class="a-section promotions-section"> <section class="a-section promotions-section">
<h2>Акции</h2> <h2>Акции</h2>
</section> </section>
<section class="a-section new-section"> <section class="a-section new-section">

View File

@@ -0,0 +1,69 @@
{% extends "_layouts/base.html" %}
{% load static %}
{% block title %}{{ item.name }}{% endblock %}
{% block add_css %}
<link rel="stylesheet" href="{% static "css/catalog.css" %}">
{% endblock %}
{% block content %}
{% include "_fragments/header.html" with active="catalog" %}
{% url 'detail' item.category.slug item.slug as product_url %}
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/catalog/' text_2='Каталог' url_3='/catalog/'|add:item.category.slug|add:'/' text_3=item.category.name url_4=product_url text_4=item.name %}
<main class="container product-detail">
<section class="product-gallery" aria-label="Изображения товара">
<div class="product-gallery__main">
{% if primary_image %}
<a href="{{ primary_image.image.url }}" data-fancybox="product-gallery">
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
</a>
{% else %}
<div class="product-gallery__placeholder">Нет фото</div>
{% endif %}
</div>
{% if images %}
<div class="product-gallery__thumbs">
{% for image in images %}
<a class="product-gallery__thumb{% if forloop.first %} active{% endif %}" href="{{ image.image.url }}" data-fancybox="product-gallery">
<img src="{{ image.image.url }}" alt="{{ image.alt|default:item.name }}">
</a>
{% endfor %}
</div>
{% endif %}
</section>
<section class="product-summary" aria-label="Информация о товаре">
<p class="product-summary__category">{{ item.category.name }}</p>
<h1 class="product-summary__title">{{ item.name }}</h1>
<div class="product-summary__meta">
<span class="product-summary__price">{{ item.price }} руб.</span>
<span class="product-summary__availability {% if item.is_available %}available{% else %}unavailable{% endif %}">
{% if item.is_available %}В наличии{% else %}Нет в наличии{% endif %}
</span>
</div>
<div class="product-summary__swatches" aria-hidden="true">
<span class="product-summary__swatch product-summary__swatch--dark"></span>
<span class="product-summary__swatch product-summary__swatch--graphite"></span>
<span class="product-summary__swatch product-summary__swatch--taupe"></span>
<span class="product-summary__swatch product-summary__swatch--sand"></span>
</div>
{% if item.description %}
<div class="product-summary__description">{{ item.description|linebreaksbr }}</div>
{% endif %}
<div class="product-summary__actions">
<button class="product-summary__button product-summary__button--primary" type="button">В корзину</button>
<button class="product-summary__button product-summary__button--secondary" type="button">Купить</button>
</div>
</section>
</main>
{% include "_fragments/footer.html" %}
{% endblock %}

View File

@@ -18,7 +18,7 @@
{% endif %} {% endif %}
<div class="container catalog-container"> <div class="container catalog-container">
{% include 'catalog/_fragments/side-bar.html' with categories=categories %} {% include 'catalog/_fragments/side-bar.html' with categories=categories selected_category=selected_category %}
{% include 'catalog/_fragments/item-list.html' with items=items %} {% include 'catalog/_fragments/item-list.html' with items=items %}
</div> </div>