diff --git a/config/urls.py b/config/urls.py index 1ca01ec70..450c8ef8d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -28,6 +28,7 @@ CommunityView, DocLibsTemplateView, ImageView, + LearnPageView, MarkdownTemplateView, TermsOfUseView, PrivacyPolicyView, @@ -181,6 +182,7 @@ ), path("health/", include("health_check.urls")), path("asciidoctor_sandbox/", include("asciidoctor_sandbox.urls")), + path("learn/", LearnPageView.as_view(), name="learn"), path("community/", CommunityView.as_view(), name="community"), path( "community//", diff --git a/core/context_processors.py b/core/context_processors.py index 23bc7b20b..2008a8511 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -181,6 +181,7 @@ class NavItem(StrEnum): "/doc/libs/": NavItem.LIBRARIES, # special case - handle first "/doc/": NavItem.LEARN, "/docs/": NavItem.LEARN, + "/learn/": NavItem.LEARN, "/boost-development/": NavItem.LEARN, "/news/": NavItem.NEWS, "/community/": NavItem.COMMUNITY, @@ -217,7 +218,7 @@ def header_context(request): """Context processor for header nav links.""" nav_links = [ NavLink(label="Libraries", url=reverse("libraries"), nav_id="libraries"), - NavLink(label="Learn", url=reverse("docs"), nav_id="learn"), + NavLink(label="Learn", url=reverse("learn"), nav_id="learn"), NavLink(label="Community", url=reverse("community"), nav_id="community"), NavLink( label="Posts", url=reverse("news"), nav_id="news", is_unread=True diff --git a/core/views.py b/core/views.py index 2ede958a7..debef541e 100644 --- a/core/views.py +++ b/core/views.py @@ -1,7 +1,9 @@ import os +import random import re import requests +from django.db.models import Count from django.utils import timezone from textwrap import dedent @@ -86,8 +88,10 @@ save_rendered_content, ) -from libraries.models import Library, LibraryVersion, Tier + +from libraries.models import Category, Library, LibraryVersion, Tier from news.models import Entry +from news.services import get_latest_post_cards from libraries.utils import ( get_commit_data_by_release_for_library, commit_data_to_stats_bars, @@ -506,155 +510,173 @@ def get_v3_context_data(self, **kwargs): ctx["learn_card_data"] = [ { "title": "I want to learn:", - "text": "How to install Boost, use its libraries, build projects, and get help when you need it.", + "text": ( + "How to install Boost, use its libraries, build projects, " + "and get help when you need it." + ), "links": [ { "label": "Explore common use cases", - "url": "https://www.example.com", + "url": "https://www.boost.org/doc/user-guide/common-introduction.html", + }, + { + "label": "Build with CMake", + "url": "https://www.boost.org/doc/user-guide/building-with-cmake.html", + }, + { + "label": "Visit the FAQ", + "url": "https://www.boost.org/doc/user-guide/faq.html", }, - {"label": "Build with CMake", "url": "https://www.example.com"}, - {"label": "Visit the FAQ", "url": "https://www.example.com"}, ], - "url": "https://www.example.com", - "label": "Learn more about Boost", + "url": "https://www.boost.org/doc/user-guide/getting-started.html", + "label": "Get started with Boost", "image_src": large_static("/img/v3/learn-page/learn-cheetah.png"), "mobile_image_src": large_static( "/img/v3/learn-page/cheetah-mobile.png" ), }, { - "title": "I want to learn:", - "text": "How to install Boost, use its libraries, build projects, and get help when you need it.", + "title": "I want to contribute:", + "text": ( + "How to contribute to Boost, propose new libraries, " + "and engage in formal reviews." + ), "links": [ { - "label": "Explore common use cases", - "url": "https://www.example.com", + "label": "Contribute to an existing library", + "url": "https://www.boost.org/doc/contributor-guide/contributors-faq.html", + }, + { + "label": "Learn about formal reviews", + "url": "https://www.boost.org/doc/formal-reviews/submissions.html", + }, + { + "label": "Visit the Contributors FAQ", + "url": "https://www.boost.org/doc/contributor-guide/contributors-faq.html", }, - {"label": "Build with CMake", "url": "https://www.example.com"}, - {"label": "Visit the FAQ", "url": "https://www.example.com"}, ], - "url": "https://www.example.com", - "label": "Learn more about Boost", - "image_src": large_static("img/v3/learn-page/learn-octopus.png"), + "url": "https://www.boost.org/doc/contributor-guide/requirements/library-requirements.html", + "label": "Propose a new library", + "image_src": large_static("/img/v3/learn-page/learn-octopus.png"), "mobile_image_src": large_static( "/img/v3/learn-page/octopus-mobile.png" ), }, ] - demo_cards = [ - { - "title": "Get help", - "description": "Tap into quick answers, networking, and chat with 24,000+ members.", - "cta_label": "Start here", - "cta_href": reverse("community"), - }, - { - "title": "Documentation", - "description": "Browse library docs, examples, and release notes in one place.", - "cta_label": "View docs", - "cta_href": reverse("docs"), - }, - { - "title": "Community", - "description": "Mailing lists, GitHub, and community guidelines for contributors.", - "cta_label": "Join", - "cta_href": reverse("community"), - }, - { - "title": "Releases", - "description": "Latest releases, download links, and release notes.", - "cta_label": "Download", - "cta_href": reverse("releases-most-recent"), - }, + ctx["library_cards"] = self._build_category_cards() + + # Copy mirrors the Figma frame at node 1849:49695. The design currently + # has two cards titled "Modern approach to C++" — flagged to design. + ctx["why_boost_cards"] = [ { - "title": "Libraries", - "description": "Explore the full catalog of Boost C++ libraries with docs and metadata.", - "cta_label": "Browse libraries", - "cta_href": reverse("libraries"), + "title": "Modern approach to C++", + "description": "Skip months of development with production-ready solutions.", }, { - "title": "News", - "description": "Blog posts, announcements, and community news from the Boost project.", - "cta_label": "Read news", - "cta_href": reverse("news"), + "title": "Write safer code", + "description": "Peer-reviewed by C++ standards committee members.", }, { - "title": "Getting started", - "description": "Step-by-step guides to build and use Boost in your projects.", - "cta_label": "Get started", - "cta_href": reverse("getting-started"), + "title": "Make real impact", + "description": "Your code could power millions of apps & shape the future of C++.", }, { - "title": "Resources", - "description": "Learning resources, books, and other materials for Boost users.", - "cta_label": "View resources", - "cta_href": reverse("resources"), + "title": "Modern approach to C++", + "description": "We embrace contemporary C++ practices, leveraging the latest language features.", }, { - "title": "Calendar", - "description": "Community events, meetings, and review schedule.", - "cta_label": "View calendar", - "cta_href": reverse("calendar"), + "title": "Template-heavy by design", + "description": "Maximum flexibility with compile-time guarantees.", }, { - "title": "Donate", - "description": "Support the Boost Software Foundation and open-source C++.", - "cta_label": "Donate", - "cta_href": reverse("donate"), + "title": "Independent but consistent", + "description": "Each library is built by a small, dedicated team with shared standards.", }, ] - ctx["library_cards"] = demo_cards - ctx["why_boost_cards"] = demo_cards[:6] ctx["calendar_card"] = { "title": "Boost is released three times a year", - "text": "Each release has updates to existing libraries, and any new libraries that have passed the rigorous acceptance process.", - "primary_button_url": "www.example.com", - "primary_button_label": "View the Release Calendar", - "secondary_button_url": "www.example.com", - "secondary_button_label": "Secondary Button", + "text": ( + "Each release has updates to existing libraries, and any new " + "libraries that have passed the rigorous acceptance process." + ), + "primary_button_url": reverse("calendar"), + "primary_button_label": "View release calendar", "image": large_static("/img/v3/demo-page/calendar.png"), } ctx["info_card"] = { "title": "How we got here", - "text": "Since 1998, Boost has been where C++ innovation happens. What started with three developers has grown into the foundation of modern C++ development.", - "primary_button_url": "www.example.com", - "primary_button_label": "Explore Our History", + "text": ( + "Since 1998, Boost has been where C++ innovation happens. What " + "started with three developers has grown into the foundation of " + "modern C++ development." + ), + "primary_button_url": "https://www.boost.org/doc/user-guide/boost-history.html", + "primary_button_label": "Explore our history", } ctx["post_cards_data"] = { - "heading": "Posts from the Boost Community", - "view_all_url": "#", - "view_all_label": "View All Posts", - "variant": "Content Card", - "posts": SharedResources.demo_posts[0:4], + "heading": "Posts from the Boost community", + "view_all_url": reverse("news"), + "view_all_label": "View all posts", + "posts": get_latest_post_cards(limit=4), } ctx["boost_community_data"] = { "heading": "The Boost community", - "view_all_url": "#", - "view_all_label": "Explore the community", - "posts": 3 - * [ + "primary_cta_label": "Explore the community", + "primary_cta_url": reverse("community"), + "items": [ { - "title": "A talk by Richard Thomson at the Utah C++ Programmers Group", - "description": "Lorem Ispum Sum Delores", - "url": "#", - "date": "03/03/2025", - "category": "Issues", - "tag": "beast", - "author": { - "name": "Richard Thomson", - "role": "Contributor", - "show_badge": True, - "avatar_url": large_static("img/v3/demo-page/avatar.png"), - }, - "cta_url": "#", - "cta_label": "Learn More", - } + "title": "Stay up to date", + "description": "Watch the Boost GitHub org for the latest releases and activity.", + "cta_url": "https://github.com/boostorg/boost", + "cta_label": "Watch now", + }, + { + "title": "Get help", + "description": "Chat with thousands of C++ developers on the CPPLang Slack.", + "cta_url": "https://cppalliance.org/slack/", + "cta_label": "Join now", + }, + { + "title": "Fix an issue", + "description": "Browse open issues and contribute fixes on GitHub.", + "cta_url": "https://github.com/boostorg/boost/issues", + "cta_label": "Get involved", + }, ], } return ctx + @staticmethod + def _build_category_cards(): + """Return library categories shuffled per request, with live counts.""" + categories = list( + Category.objects.annotate(library_count=Count("libraries", distinct=True)) + .filter(library_count__gt=0) + .only("name", "slug", "short_description") + ) + random.shuffle(categories) + cards = [] + for category in categories: + cards.append( + { + "title": category.name, + "description": category.short_description, + "badge_count": category.library_count, + "cta_label": "Start here", + "cta_href": reverse( + "libraries-list", + kwargs={ + "version_slug": LATEST_RELEASE_URL_PATH_STR, + "library_view_str": "categorized", + "category_slug": category.slug, + }, + ), + } + ) + return cards + class ContentNotFoundException(Exception): pass diff --git a/libraries/admin.py b/libraries/admin.py index e0a1b10d2..db67c9b64 100644 --- a/libraries/admin.py +++ b/libraries/admin.py @@ -162,9 +162,10 @@ def merge_authors(self, request, queryset): @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): - list_display = ["name"] + list_display = ["name", "short_description"] ordering = ["name"] search_fields = ["name"] + fields = ["name", "slug", "short_description"] class LibraryVersionInline(admin.TabularInline): diff --git a/libraries/migrations/0040_category_short_description.py b/libraries/migrations/0040_category_short_description.py new file mode 100644 index 000000000..245f901ab --- /dev/null +++ b/libraries/migrations/0040_category_short_description.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.2 on 2026-05-14 16:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0039_flag_known_bot_commit_authors"), + ] + + operations = [ + migrations.AddField( + model_name="category", + name="short_description", + field=models.TextField( + blank=True, + default="", + help_text="Short marketing copy shown on the Learn page category carousel.", + ), + ), + ] diff --git a/libraries/models.py b/libraries/models.py index c1619f5ce..6604aafe1 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -50,6 +50,11 @@ class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(blank=True, null=True) + short_description = models.TextField( + blank=True, + default="", + help_text="Short marketing copy shown on the Learn page category carousel.", + ) class Meta: verbose_name_plural = "Categories" diff --git a/libraries/views.py b/libraries/views.py index 4f6ead4ee..c5ff38c1f 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -17,7 +17,7 @@ from core.githubhelper import GithubAPIClient from core.mixins import V3Mixin from core.mock_data import SharedResources -from news.models import Entry +from news.services import get_latest_post_cards from versions.exceptions import BoostImportedDataException from versions.models import Version @@ -510,30 +510,7 @@ def get_v3_context_data(self, base_context=None, **kwargs): version_str, ) - context["library_posts"] = [ - { - "title": entry.title, - "url": entry.get_absolute_url(), - "date": entry.publish_at, - "category": entry.determined_news_type or "news", - "tag": "", - "author": { - "name": getattr(entry.author, "display_name", None) - or str(entry.author), - "profile_url": None, - "role": "Author", - "avatar_url": ( - entry.author.get_avatar_url() - if hasattr(entry.author, "get_avatar_url") - else "" - ), - "badge_url": None, - }, - } - for entry in Entry.objects.published() - .select_related("author") - .order_by("-publish_at")[:3] - ] + context["library_posts"] = get_latest_post_cards(limit=3) this_release = ( [u.to_v3_profile_dict("Author") for u in base_context.get("authors", [])] diff --git a/libraries/wagtail_hooks.py b/libraries/wagtail_hooks.py new file mode 100644 index 000000000..a001aebe9 --- /dev/null +++ b/libraries/wagtail_hooks.py @@ -0,0 +1,21 @@ +from wagtail.admin.panels import FieldPanel +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import SnippetViewSet + +from .models import Category + + +class CategorySnippetViewSet(SnippetViewSet): + model = Category + menu_label = "Library Categories" + icon = "tag" + list_display = ["name", "short_description"] + search_fields = ["name", "short_description"] + panels = [ + FieldPanel("name"), + FieldPanel("slug"), + FieldPanel("short_description"), + ] + + +register_snippet(CategorySnippetViewSet) diff --git a/news/services.py b/news/services.py new file mode 100644 index 000000000..3bfdd8f61 --- /dev/null +++ b/news/services.py @@ -0,0 +1,37 @@ +from .models import Entry + + +def _entry_to_post_card(entry: Entry) -> dict: + author = entry.author + return { + "title": entry.title, + "url": entry.get_absolute_url(), + "date": entry.publish_at, + "category": entry.determined_news_type or "news", + "tag": "", + "author": { + "name": getattr(author, "display_name", None) or str(author), + "profile_url": None, + "role": "Author", + "avatar_url": ( + author.get_avatar_url() if hasattr(author, "get_avatar_url") else "" + ), + "badge_url": None, + "show_badge": False, + }, + } + + +def get_latest_post_cards(limit: int = 3) -> list[dict]: + """Return the most recent published entries as v3 post-card dicts. + + Shared by the Learn page, library detail, community page, and any other + surface that renders the v3 latest-posts card. Keeps the dict shape + consistent so downstream templates don't drift. + """ + queryset = ( + Entry.objects.published() + .select_related("author") + .order_by("-publish_at")[:limit] + ) + return [_entry_to_post_card(entry) for entry in queryset] diff --git a/static/css/v3/learn-cards.css b/static/css/v3/learn-cards.css index 0f1a90bd6..11636a9b9 100644 --- a/static/css/v3/learn-cards.css +++ b/static/css/v3/learn-cards.css @@ -31,6 +31,10 @@ .learn-card__link-column { margin-bottom: var(--space-xxl); } + + .learn-card .card__cta_section .btn-hero { + min-height: 0; + } } .learn-card__column { diff --git a/templates/v3/includes/_cards_carousel_v3.html b/templates/v3/includes/_cards_carousel_v3.html index 6489d7d55..2001a740c 100644 --- a/templates/v3/includes/_cards_carousel_v3.html +++ b/templates/v3/includes/_cards_carousel_v3.html @@ -6,8 +6,9 @@ carousel_id (required) — id for the carousel root and controls. heading (required) — section heading. track_aria_label (optional) — aria-label for the track. Default: "Detail cards carousel" - cards (optional) — list of dicts with: title, description, icon_name, cta_label, cta_href. + cards (optional) — list of dicts with: title, description, icon_name, cta_label, cta_href, badge_count. When omitted, no cards are shown (pass from view for content). + If badge_count is omitted on a card, the loop index is used (legacy behavior). infinite (optional) — if True, carousel loops infinitely. Default: False autoplay (optional) — if True, carousel scrolls automatically. Default: False autoplay_delay (optional) — autoplay interval in milliseconds. Only used when autoplay is True. Default: 4000 @@ -39,7 +40,7 @@

{{ item.title }}

{% endif %} {% if item.url %} - Start here + {{ item.cta_label|default:"Start here" }} {% endif %} diff --git a/templates/v3/learn_page.html b/templates/v3/learn_page.html index f45b32e66..a38d10b15 100644 --- a/templates/v3/learn_page.html +++ b/templates/v3/learn_page.html @@ -30,12 +30,12 @@

Start anywhere. Learn everything. Build anything.

{% with data=post_cards_data %} - {% include 'v3/includes/_post_card.html' with heading=data.heading items=data.posts primary_cta_url=data.view_all_url primary_cta_label=data.view_all_label theme='teal' variant="card" %} + {% include 'v3/includes/_post_card.html' with heading=data.heading items=data.posts variant="card" theme="teal" layout="vertical" primary_cta_label=data.view_all_label primary_cta_url=data.view_all_url %} {% endwith %}
{% with data=boost_community_data %} - {% include 'v3/includes/_community_card.html' with heading=data.heading items=data.posts primary_cta_url=data.view_all_url primary_cta_label=data.view_all_label %} + {% include 'v3/includes/_community_card.html' with heading=data.heading items=data.items variant="card" theme="default" layout="vertical" primary_cta_label=data.primary_cta_label primary_cta_url=data.primary_cta_url %} {% endwith %}