diff --git a/innovedus_cms/home/models.py b/innovedus_cms/home/models.py index d62d0ba..d88b933 100644 --- a/innovedus_cms/home/models.py +++ b/innovedus_cms/home/models.py @@ -23,6 +23,19 @@ BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 7) # Default to 7 articles in b HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 4) # Default to 4 articles in horizon layout PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination CATEGORY_HOT_SIZE = _get_env_int("CATEGORY_HOT_SIZE", 6) # Default to 4 articles in category hot layout +RELATED_ARTICLES_SIZE = _get_env_int("RELATED_ARTICLES_SIZE", 4) # Default to 4 related articles + +class BreadcrumbMixin: + def build_breadcrumbs(self): + site = self.get_site() + site_root = site.root_page if site else None + if site_root: + ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth) + else: + ancestors = self.get_ancestors().specific() + breadcrumbs = list(ancestors) + [self] + return breadcrumbs, site_root + # Mixin for Category-related functionality class CategoryMixin: @@ -70,17 +83,6 @@ class CategoryMixin: ) return blocks - # Build breadcrumbs - def build_breadcrumbs(self): - site = self.get_site() - site_root = site.root_page if site else None - if site_root: - ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth) - else: - ancestors = self.get_ancestors().specific() - breadcrumbs = list(ancestors) + [self] - return breadcrumbs, site_root - # Get latest articles def get_latest_articles(self, request=None): latest_page = LatestPage.objects.first() @@ -196,7 +198,7 @@ class HomePage(Page, CategoryMixin): return context -class LatestPage(Page, CategoryMixin): +class LatestPage(Page, CategoryMixin, BreadcrumbMixin): template = "home/category_page.html" def get_context(self, request): @@ -208,7 +210,7 @@ class LatestPage(Page, CategoryMixin): return context -class TrendingPage(Page, CategoryMixin): +class TrendingPage(Page, CategoryMixin, BreadcrumbMixin): template = "home/category_page.html" def get_context(self, request): @@ -220,7 +222,7 @@ class TrendingPage(Page, CategoryMixin): return context -class CategoryPage(Page, CategoryMixin): +class CategoryPage(Page, CategoryMixin, BreadcrumbMixin): @property def has_subcategories(self): return self.get_children().type(CategoryPage).live().exists() @@ -254,7 +256,7 @@ class ArticlePageTag(TaggedItemBase): on_delete=models.CASCADE, ) -class ArticlePage(Page): +class ArticlePage(Page, BreadcrumbMixin): cover_image = models.ForeignKey( "wagtailimages.Image", null=True, @@ -308,6 +310,16 @@ class ArticlePage(Page): def get_context(self, request): context = super().get_context(request) + breadcrumbs, site_root = self.build_breadcrumbs() + context["breadcrumbs"] = breadcrumbs + context["breadcrumb_root"] = site_root + category_crumbs = [ + crumb + for crumb in breadcrumbs + if crumb.id != self.id and (not site_root or crumb.id != site_root.id) + ] + context["article_category"] = category_crumbs[-1] if category_crumbs else None + tag_ids = list(self.tags.values_list("id", flat=True)) if tag_ids: related_articles = ( @@ -315,7 +327,7 @@ class ArticlePage(Page): .exclude(id=self.id) .filter(tags__id__in=tag_ids) .distinct() - .order_by("-date", "-id")[:4] + .order_by("-date", "-id")[:RELATED_ARTICLES_SIZE] ) else: related_articles = ArticlePage.objects.none() diff --git a/innovedus_cms/home/static/css/article_page.css b/innovedus_cms/home/static/css/article_page.css new file mode 100644 index 0000000..4c50733 --- /dev/null +++ b/innovedus_cms/home/static/css/article_page.css @@ -0,0 +1,396 @@ +.article-page { + width: 100%; + max-width: 718px; + margin: 0 auto; +} + +.article-breadcrumbs { + margin: 12px 0 16px; +} + +.article-breadcrumbs ol { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0; + margin: 0; + padding: 0; + list-style: none; +} + +.article-breadcrumbs li { + font-size: 12px; + font-weight: 700; + line-height: 1.5; + color: #0e1b4266; +} + +.article-breadcrumbs li + li::before { + content: "/"; + margin: 0 8px; +} + +.article-breadcrumbs a:hover { + text-decoration: underline; +} + +.block-title { + background-color: #0e1b42; + color: #ffffff; +} + +.article-content img { + max-width: 718px; + height: auto; + display: block; + margin: 16px 0; +} + +.article-content h1 { + font-size: 36px; + margin: 24px 0; +} + +.article-content h2 { + font-size: 32px; + margin: 24px 0; +} + +.article-content p { + font-size: 14px; +} + +.article-content .intro { + font-size: 24px; +} + +.article-content .body iframe { + display: block; + width: 100% !important; + max-width: 100% !important; + border: 0; +} + +.article-content .body blockquote.instagram-media, +.article-content .body blockquote.twitter-tweet, +.article-content .body .fb-post, +.article-content .body .fb-video { + max-width: 100% !important; + min-width: 0 !important; + margin-left: auto !important; + margin-right: auto !important; +} + +.article-content .date { + display: flex; + align-items: center; + font-size: 24px; + color: #0e1b4266; + margin: 24px 0; +} + +.article-content .date::after { + content: ""; + flex: 1; + height: 1px; + background: #0e1b4266; + margin-left: 16px; +} + +.article-tags { + display: flex; + justify-content: flex-end; + margin: 24px 0; +} + +.article-tags .tag-chips { + flex-wrap: nowrap; + white-space: nowrap; + overflow-x: auto; +} + +.tag-chips li { + flex: 0 0 auto; +} + +.tag-chips a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 96px; + height: 25px; + color: #ffffff; + font-size: 10px; + font-weight: 800; + line-height: 1; + overflow: hidden; +} + +.tag-chips li:nth-child(3n + 1) a { + background: #00abf5; +} + +.tag-chips li:nth-child(3n + 2) a { + background: #f4a41c; +} + +.tag-chips li:nth-child(3n) a { + background: #ff461c; +} + +.article-share { + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + margin: 64px 0; +} + +.article-share .icon { + width: 32px; + height: 32px; + --fill-0: #0e1b4266; +} + +.article-share .icon .icon-cutout { + fill: #ffffff; +} + +.related-articles { + margin-top: 32px; + margin-bottom: 32px; +} + +.related-articles h2 { + margin: 0 0 24px; + text-align: center; + font-size: 24px; + font-weight: 600; + color: #0e1b4266; +} + +.related-articles-list { + display: flex; + flex-direction: column; + gap: 24px; +} + +.related-articles-list > hr { + width: 100%; + margin: 12px 0; + margin-inline: 0; + border: 0; + border-top: 1px solid #0e1b4266; +} + +.related-article-item { + display: grid; + grid-template-columns: 194px 1fr; + gap: 24px; + align-items: start; +} + +.related-article-image { + display: block; + width: 194px; +} + +.related-article-image img { + width: 194px; + max-width: 100%; + height: 133px; + display: block; + object-fit: cover; + border-radius: 5px; +} + +.related-article-date { + margin: 10px 0 0; + text-align: left; + font-size: 16px; + color: #0e1b4266; +} + +.related-article-title { + margin: 0 0 12px; + font-size: 28px; + line-height: 1.3; +} + +.tag-chips { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.related-article-tags { + flex-wrap: wrap; + gap: 8px 12px; +} + +.back-to-top { + position: fixed; + right: var(--back-to-top-right, 24px); + bottom: 24px; + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 0; + border: 0; + background: transparent; + color: #0e1b42; + cursor: pointer; + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.back-to-top.is-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.back-to-top__icon { + position: relative; + display: block; + width: 48px; + height: 48px; + background: #ffffff; + border: 1px solid #d9deea; + box-shadow: 0 3px 8px rgba(14, 27, 66, 0.18); +} + +.back-to-top__icon::before, +.back-to-top__icon::after { + content: ""; + position: absolute; + top: 24px; + width: 14px; + height: 2px; + background: #0e1b42; +} + +.back-to-top__icon::before { + left: 12px; + transform: rotate(-45deg); +} + +.back-to-top__icon::after { + right: 12px; + transform: rotate(45deg); +} + +.back-to-top__label { + font-size: 24px; + line-height: 1; +} + +@media (min-width: 768px) and (max-width: 1023px) { + .article-page { + max-width: 560px; + } + + .article-content img { + max-width: 560px; + } + + .article-content h2 { + font-size: 24px; + } +} + +@media (min-width: 575px) and (max-width: 767px) { + .article-page { + max-width: 426px; + } + + .article-content img { + max-width: 426px; + } + + .related-article-title { + font-size: 20px; + } + + .back-to-top { + bottom: 20px; + } +} + +@media (max-width: 574px) { + .article-page { + max-width: 300px; + } + + .article-content img { + max-width: 300px; + } + + .article-content h1 { + font-size: 24px; + } + + .article-content .intro { + font-size: 18px; + } + + .article-content .date { + font-size: 16px; + } + + .article-tags { + justify-content: center; + } + + .related-article-item { + grid-template-columns: 139px 1fr; + gap: 12px; + } + + .related-article-image { + width: 139px; + } + + .related-article-image img { + width: 139px; + height: 110px; + } + + .related-article-date { + font-size: 12px; + } + + .related-article-title { + font-size: 16px; + } + + .tag-chips a { + width: 70px; + } + + .related-article-tags { + flex-wrap: wrap; + gap: 6px 8px; + } + + .back-to-top { + bottom: 16px; + } + + .back-to-top__label { + font-size: 18px; + } +} + +@media (max-width: 767px) { + .article-breadcrumbs { + margin: 8px 0 12px; + } +} diff --git a/innovedus_cms/home/static/css/category.css b/innovedus_cms/home/static/css/category.css index 20e3e37..a748c31 100644 --- a/innovedus_cms/home/static/css/category.css +++ b/innovedus_cms/home/static/css/category.css @@ -1,17 +1,3 @@ -.block-title { - display: inline-block; - width: 197px; - height: 87px; - vertical-align: middle; - font-size: 20px; - font-weight: 700; -} - -.block-title span { - padding-left: 21px; - line-height: 87px; -} - .category-title { background-color: #00abf5; color: #ffffff; @@ -50,17 +36,6 @@ } @media (max-width: 574px) { - .block-title { - width: 139px; - height: 55px; - font-size: 16px; - } - - .block-title span { - padding-left: 14px; - line-height: 55px; - } - .subcategory-title span { width: 139px; height: 55px; diff --git a/innovedus_cms/home/static/css/home.css b/innovedus_cms/home/static/css/home.css index 07a05e1..81a1f45 100644 --- a/innovedus_cms/home/static/css/home.css +++ b/innovedus_cms/home/static/css/home.css @@ -13,20 +13,6 @@ margin: 10px 0; } -.block-title { - display: inline-block; - width: 197px; - height: 87px; - vertical-align: middle; - font-size: 20px; - font-weight: 700; -} - -.block-title span { - padding-left: 21px; - line-height: 87px; -} - .block-title-divider { display: inline-flex; width: 28px; @@ -78,17 +64,6 @@ } @media (max-width: 574px) { - .block-title { - width: 139px; - height: 55px; - font-size: 16px; - } - - .block-title span { - padding-left: 14px; - line-height: 55px; - } - .block-title-divider { width: 20px; height: 1px; diff --git a/innovedus_cms/home/static/js/article_page.js b/innovedus_cms/home/static/js/article_page.js new file mode 100644 index 0000000..bbd53ae --- /dev/null +++ b/innovedus_cms/home/static/js/article_page.js @@ -0,0 +1,67 @@ +(function () { + const button = document.querySelector(".back-to-top"); + const articlePage = document.querySelector(".article-page"); + const articleBody = document.querySelector(".article-content .body"); + if (!button) return; + + const toggleVisibility = () => { + button.classList.toggle("is-visible", window.scrollY > 200); + }; + + const syncHorizontalPosition = () => { + if (!articlePage) return; + const rightGap = Math.max(0, window.innerWidth - articlePage.getBoundingClientRect().right); + button.style.setProperty("--back-to-top-right", `${rightGap}px`); + }; + + const makeEmbedsResponsive = () => { + if (!articleBody) return; + + articleBody.querySelectorAll("iframe").forEach((iframe) => { + const src = (iframe.getAttribute("src") || "").toLowerCase(); + const isFixedRatioPlayer = + src.includes("youtube.com") || + src.includes("youtube-nocookie.com") || + src.includes("m.youtube.com") || + src.includes("youtu.be") || + src.includes("vimeo.com") || + src.includes("player.vimeo.com") || + src.includes("dailymotion.com"); + const width = Number(iframe.getAttribute("width")); + const height = Number(iframe.getAttribute("height")); + + iframe.style.width = "100%"; + iframe.style.maxWidth = "100%"; + + if (isFixedRatioPlayer) { + if (width > 0 && height > 0) { + iframe.style.aspectRatio = `${width} / ${height}`; + } else { + iframe.style.aspectRatio = "16 / 9"; + } + iframe.style.height = "auto"; + } else { + iframe.style.removeProperty("aspect-ratio"); + iframe.style.removeProperty("height"); + } + }); + }; + + window.addEventListener("scroll", toggleVisibility, { passive: true }); + window.addEventListener("resize", syncHorizontalPosition); + + toggleVisibility(); + syncHorizontalPosition(); + makeEmbedsResponsive(); + + if (articleBody && "MutationObserver" in window) { + const observer = new MutationObserver(() => { + makeEmbedsResponsive(); + }); + observer.observe(articleBody, { childList: true, subtree: true }); + } + + button.addEventListener("click", function () { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); +})(); diff --git a/innovedus_cms/home/templates/home/article_page.html b/innovedus_cms/home/templates/home/article_page.html index f2891ab..528c3ef 100644 --- a/innovedus_cms/home/templates/home/article_page.html +++ b/innovedus_cms/home/templates/home/article_page.html @@ -1,35 +1,125 @@ {% extends "base.html" %} -{% load wagtailcore_tags wagtailimages_tags %} +{% load wagtailcore_tags wagtailimages_tags static %} + +{% block extra_css %} + +{% endblock %} {% block content %} -
-

{{ page.title }}

- {% if page.banner_image %} - {% image page.banner_image original as banner %} - {{ page.title }} +
+ {% if breadcrumbs %} + {% endif %} -

{{ page.date }}

-
{{ page.intro }}
-
- {{ page.body }} + +
+ {% if page.cover_image %} + {% image page.cover_image original as cover %} + {{ page.title }} + {% endif %} +

{{ page.title }}

+

{{ page.date|date:"Y/m/d" }}

+
{{ page.intro }}
+
+ {{ page.body }} +
+
+ {% with share_url=request.build_absolute_uri|urlencode share_text=page.title|urlencode %} +
+ + + + + + + + + + + + + + + + + +
+ {% endwith %} {% with tags=page.tags.all %} {% if tags %} -
- Hashtags: -
+ +{% endblock %} + +{% block extra_js %} + {{ block.super }} + {% endblock %} diff --git a/innovedus_cms/mysite/static/css/mysite.css b/innovedus_cms/mysite/static/css/mysite.css index bf79be7..57bc3db 100644 --- a/innovedus_cms/mysite/static/css/mysite.css +++ b/innovedus_cms/mysite/static/css/mysite.css @@ -369,6 +369,20 @@ footer .footer-sections { flex-wrap: wrap; } +.block-title { + display: inline-block; + width: 197px; + height: 87px; + vertical-align: middle; + font-size: 20px; + font-weight: 700; +} + +.block-title span { + padding-left: 21px; + line-height: 87px; +} + .template-darkbackground .site-hero-band { background-color: #0e1b42; color: #ffffff; @@ -578,6 +592,17 @@ footer .footer-sections { .main-nav { right: 16px; } + + .block-title { + width: 139px; + height: 55px; + font-size: 16px; + } + + .block-title span { + padding-left: 14px; + line-height: 55px; + } } @layer figreset {