diff --git a/lpon_site/frontend/views.py b/lpon_site/frontend/views.py
index c0e75dd..6553d81 100644
--- a/lpon_site/frontend/views.py
+++ b/lpon_site/frontend/views.py
@@ -1,13 +1,108 @@
#
+from dataclasses import dataclass
+
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse
# Create your views here.
+
+@dataclass(frozen=True)
+class BreadcrumbItem:
+ """
+ Один пункт хлебных крошек (например, "Главная", "Каталог", "Винил").
+
+ ЧТО ТАКОЕ dataclass (если видите это в первый раз)
+ ----------------------------------------------------
+ `@dataclass` — это декоратор из стандартной библиотеки Python (модуль `dataclasses`).
+ Он берёт класс, в котором просто перечислены имена полей и их типы (без ручного
+ __init__), и САМ генерирует за вас служебные методы:
+ - __init__(self, title, url=None) — конструктор класса. Благодаря нему можно
+ писать BreadcrumbItem(title="Каталог", url="/catalog") без единой строчки
+ кода конструктора;
+ - __repr__ — красивое текстовое представление для print()/логов, например:
+ BreadcrumbItem(title='Каталог', url='/catalog');
+ - __eq__ — сравнение двух объектов по значениям полей, то есть
+ BreadcrumbItem('Каталог', '/catalog') == BreadcrumbItem('Каталог', '/catalog')
+ вернёт True (без dataclass пришлось бы сравнивать id() объектов).
+
+ Без dataclass пришлось бы писать руками:
+
+ class BreadcrumbItem:
+ def __init__(self, title, url=None):
+ self.title = title
+ self.url = url
+
+ С dataclass — просто пишем поля (см. ниже) и всё перечисленное выше Python
+ сгенерирует сам. Это НЕ меняет то, как объект используется — просто экономит
+ код и снижает риск ошибок при ручном написании __init__/__repr__/__eq__.
+
+ Чем dataclass отличается от NamedTuple (typing.NamedTuple)?
+ ---------------------------------------------------------------
+ Обе конструкции решают одну и ту же задачу — описать "структуру из нескольких
+ полей" без лишнего кода, но по-разному устроены внутри:
+ - `NamedTuple` — это, по сути, обычный tuple с именами полей. Он ВСЕГДА
+ неизменяем (immutable), поддерживает распаковку как обычный tuple
+ (title, url = item) и итерацию (for value in item);
+ - `dataclass` — это обычный класс. По умолчанию он изменяем (можно
+ присвоить item.title = "..." в любой момент), но можно сделать
+ неизменяемым через параметр frozen=True (как сделано ниже). В отличие
+ от NamedTuple, dataclass НЕ поддерживает распаковку как tuple, зато его
+ проще расширять методами, наследованием и сложными полями по умолчанию
+ (списками, словарями и т.п.).
+
+ Для хлебных крошек подошёл бы любой из двух вариантов. Здесь выбран dataclass
+ с frozen=True — то есть объект ведёт себя как неизменяемый: попытка сделать
+ `item.title = "другое значение"` после создания вызовет исключение. Это
+ логично: один и тот же пункт крошек не должен "мутировать" по ходу рендеринга
+ страницы.
+
+ КОНТРАКТ С ШАБЛОНОМ (важно!):
+ -------------------------------
+ Шаблон lpon_site/templates/block/breadcrumbs.html ожидает контекстную
+ переменную `breadcrumbs` — список объектов именно с такими двумя полями:
+
+ Атрибуты:
+ title (str):
+ Видимый текст пункта, например "Каталог", "Винил".
+ url (str | None):
+ Ссылка на пункт. Если None (значение по умолчанию) — пункт
+ считается ТЕКУЩЕЙ страницей: шаблон покажет его БЕЗ ссылки, с
+ атрибутом aria-current="page". Обычно url=None указывают только
+ у ПОСЛЕДНЕГО пункта в списке крошек.
+
+ ВАЖНО: пункт "Главная" в этот список включать НЕ нужно — ссылку на
+ главную страницу (в виде иконки домика) шаблон breadcrumbs.html
+ добавляет сам, одинаково для всех страниц. Список крошек должен
+ начинаться сразу со следующего уровня (например, с "Каталог").
+
+ Пример использования — см. функцию catalog() ниже.
+ """
+ title: str
+ url: str | None = None
+
+
def index(request: HttpRequest | None) -> HttpResponse:
return render(request, 'index.html', {})
def catalog(request: HttpRequest | None) -> HttpResponse:
- return render(request, 'catalog.html', {})
\ No newline at end of file
+ """
+ Страница каталога (пока черновик вёрстки, см. lpon_site/templates/catalog.html).
+
+ ТЕСТОВЫЕ ДАННЫЕ ДЛЯ ХЛЕБНЫХ КРОШЕК:
+ В реальной вьюхе список крошек будет собираться динамически (в зависимости
+ от применённых фильтров, выбранной категории и т.п.). Пока каталог — черновик,
+ здесь захардкожен простой пример из двух пунктов, чтобы продемонстрировать
+ работу block/breadcrumbs.html: "Каталог" — обычный пункт со ссылкой, а
+ "Компакт-кассеты" — текущая страница (url не передан, поэтому он покажется без
+ ссылки). Пункт "Главная" передавать не нужно — его в виде иконки домика
+ сам добавляет шаблон breadcrumbs.html.
+ """
+ breadcrumbs = [
+ BreadcrumbItem(title="Каталог", url="/catalog"),
+ BreadcrumbItem(title="Компакт-кассеты", url="/catalog/compact-cassettes"), # url передан -> обычный пункт со ссылкой
+ BreadcrumbItem(title="Для перезаписи" ), # url передан -> обычный пункт со ссылкой
+ ]
+ return render(request, 'catalog.html', {"breadcrumbs": breadcrumbs})
\ No newline at end of file
diff --git a/lpon_site/templates/block/breadcrumbs.html b/lpon_site/templates/block/breadcrumbs.html
new file mode 100644
index 0000000..5d2da99
--- /dev/null
+++ b/lpon_site/templates/block/breadcrumbs.html
@@ -0,0 +1,77 @@
+{% load static %}{% comment %}
+ Шаблон "Хлебные крошки" (breadcrumbs).
+
+ ЗАЧЕМ ЭТОТ БЛОК:
+ Показывает пользователю путь от главной страницы до текущей
+ ("Главная > Каталог > Винил > The Beatles > Abbey Road"),
+ помогая ориентироваться в структуре сайта + SEO
+
+ Первый пункт (ссылка "Главная") — вставляется САМИМ этим шаблоном,
+ без участия контекста. Она одинакова абсолютно на всех страницах
+ сайта, поэтому передавать её через каждую вьюху не нужно: переменная
+ `breadcrumbs`, которую передаёт вызывающая вьюха, НЕ должна содержать
+ пункт "Главная" — начинайте сразу со следующего уровня (например,
+ "Каталог"), см. КОНТРАКТ ниже.
+
+ Вместо слова "Главная" используется готовая иконка домика из статики
+ проекта — public/static/svgs/ico-home.svg, подключаемая через Живой звук C-46, C-60, C-90 и далее… Живой звук C-46, C-60, C-90 и далее…,
+ БЕЗ подключения иконочного шрифта (Font Awesome, IcoMoon и т.п.).
+ Пока иконок в проекте немного, тянуть ради них целый шрифт (лишние
+ килограммы + лишний HTTP-запрос + зависимость от CDN) — невыгодно;
+ один SVG-файл дешевле и не требует внешних подключений.
+
+ ВАЖНО про цвет иконки: иконка из img-тега НЕ наследует
+ цвет текста и не меняется на hover — она остаётся такой, какая
+ нарисована в самом файле (чёрная заливка). Чтобы иконка оставалась
+ видимой на тёмном фоне в тёмной теме, применён CSS-фильтр dark:invert
+ (инвертирует чёрный в белый только в тёмной теме, в светлой теме
+ фильтр не применяется).
+
+ КОНТРАКТ (что именно должна передать вызывающая вьюха/шаблон):
+ Контекстная переменная `breadcrumbs` — список (или любой другой iterable)
+ объектов с двумя атрибутами (БЕЗ пункта "Главная" — его добавляет сам
+ шаблон, см. выше):
+ - title: str — видимый текст пункта ("Каталог", "Винил"...);
+ - url: str | None — ссылка на пункт.
+ Если url = None (или отсутствует/пуст) — пункт
+ считается ТЕКУЩЕЙ страницей и рендерится БЕЗ ссылки,
+ с атрибутом aria-current="page".
+ Обычно url=None указывают только у ПОСЛЕДНЕГО пункта.
+
+ В проекте для этого используется dataclass `BreadcrumbItem` — см. подробное
+ объяснение (с комментариями "для новичков") прямо в docstring класса в файле
+ lpon_site/frontend/views.py. Шаблону не важно, dataclass это, NamedTuple или
+ просто объект с нужными атрибутами — главное, чтобы у каждого элемента были
+ поля `title` и `url`.
+
+{% endcomment %}{% if breadcrumbs %}{% endif %}
diff --git a/lpon_site/templates/block/header.html b/lpon_site/templates/block/header.html
index a06627e..49bd82c 100644
--- a/lpon_site/templates/block/header.html
+++ b/lpon_site/templates/block/header.html
@@ -1,21 +1,29 @@
-{% load static %}
-
+