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, подключаемая через , + БЕЗ подключения иконочного шрифта (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 %}
- {# ЛЕВОЕ МЕНЮ #} - ВИНИЛ - - - {# /ЛЕВОЕ МЕНЮ #} - {# ЛОГОТИП В ЦЕНТРЕ #}
- - - -
{# /ЛОГОТИП В ЦЕНТРЕ #} - {# ПРАВОЕ МЕНЮ #} - CD - - - {# /ПРАВОЕ МЕНЮ #} - {# СЛОГАН #}

Живой звук C-46, C-60, C-90 и далее…

{# /СЛОГАН #} -
+{% load static %}
{% comment %} + Пункты меню — ссылки-кнопки, а не типовой текст, поэтому у каждой явно + заданы border-0 (отключает пунктирное подчёркивание из общего стиля ссылок) и свой + цвет text-slate-900/dark:text-slate-100 (см. пояснение в css/tailwind-custom.css).{% endcomment %} + {# ЛЕВОЕ МЕНЮ #} + ВИНИЛ + + + {# /ЛЕВОЕ МЕНЮ #} + {# ЛОГОТИП В ЦЕНТРЕ #}
{% comment %} + border-0 — логотип оборачивается в ссылку без текста, отключаем типовое + пунктирное подчёркивание ссылки, которое иначе появится под картинкой.{% endcomment %} + + + +
{# /ЛОГОТИП В ЦЕНТРЕ #} + {# ПРАВОЕ МЕНЮ #} + CD + + + {# /ПРАВОЕ МЕНЮ #} + {# СЛОГАН #}

Живой звук C-46, C-60, C-90 и далее…

{# /СЛОГАН #} +
diff --git a/lpon_site/templates/catalog.html b/lpon_site/templates/catalog.html index 6f6ca57..b7e78aa 100644 --- a/lpon_site/templates/catalog.html +++ b/lpon_site/templates/catalog.html @@ -3,6 +3,10 @@ {% block DESCRIPTION %}LPON — Магазин виниловых пластинок и аудиокассет{% endblock %} {% block CONTENT %} +{# Хлебные крошки. Переменная `breadcrumbs` передаётся из вьюхи catalog() #} +{# (см. lpon_site/frontend/views.py, класс BreadcrumbItem и docstring вьюхи) #} +{% include "block/breadcrumbs.html" %} +
01 Каталог
02 Блок