diff --git a/apps/core/admin.py b/apps/core/admin.py index 8c25e6d..46d5962 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -8,19 +8,36 @@ from .models import SiteBranding, SiteContact @admin.register(SiteBranding) class SiteBrandingAdmin(admin.ModelAdmin): fieldsets = ( - ("Icon", {"fields": ("icon", "icon_alt")}), + ( + "Brand assets", + { + "fields": ( + "icon", + "icon_alt", + "website_icon", + "hero_logo", + "hero_logo_alt", + ), + "description": ( + "Manage each placement independently. Removing an upload restores " + "the existing Radiuma asset for that placement." + ), + }, + ), ( "Sizes", { "fields": ("navbar_icon_size", "footer_icon_size"), - "description": "Square dimensions in pixels for each placement.", + "description": ( + "Set the logo height for each placement. Its width is calculated " + "automatically so the uploaded image is never cropped or stretched." + ), }, ), ( "Appearance", { "fields": ( - "object_fit", "show_border", "border_width", "border_color", diff --git a/apps/core/migrations/0003_sitebranding_hero_logo_sitebranding_hero_logo_alt_and_more.py b/apps/core/migrations/0003_sitebranding_hero_logo_sitebranding_hero_logo_alt_and_more.py new file mode 100644 index 0000000..7f94806 --- /dev/null +++ b/apps/core/migrations/0003_sitebranding_hero_logo_sitebranding_hero_logo_alt_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.13 on 2026-08-01 14:10 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_site_contact'), + ] + + operations = [ + migrations.AddField( + model_name='sitebranding', + name='hero_logo', + field=models.ImageField(blank=True, help_text='Large brand artwork shown in the homepage hero. Leave empty to use the existing hero image.', null=True, upload_to='branding/hero/'), + ), + migrations.AddField( + model_name='sitebranding', + name='hero_logo_alt', + field=models.CharField(blank=True, default='Radiuma', help_text='Accessible description for the homepage hero logo.', max_length=200), + ), + migrations.AddField( + model_name='sitebranding', + name='website_icon', + field=models.FileField(blank=True, help_text='Icon shown in browser tabs and bookmarks. Use a square ICO, PNG, SVG, JPG, or WebP file. Leave empty to use the default Radiuma icons.', null=True, upload_to='branding/icons/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('ico', 'png', 'svg', 'jpg', 'jpeg', 'webp'))]), + ), + migrations.AlterField( + model_name='sitebranding', + name='footer_icon_size', + field=models.PositiveSmallIntegerField(default=34, help_text='Maximum height in pixels for the footer logo. Width scales automatically.'), + ), + migrations.AlterField( + model_name='sitebranding', + name='icon', + field=models.ImageField(blank=True, help_text='Brand logo shown in the site navigation and footer. Leave empty to use the default logo.', null=True, upload_to='branding/', verbose_name='Header and footer logo'), + ), + migrations.AlterField( + model_name='sitebranding', + name='navbar_icon_size', + field=models.PositiveSmallIntegerField(default=26, help_text='Maximum height in pixels for the header logo. Width scales automatically.'), + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 4f3fb1a..875f408 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -1,3 +1,4 @@ +from django.core.validators import FileExtensionValidator from django.db import models @@ -13,20 +14,50 @@ class SiteBranding(models.Model): upload_to="branding/", blank=True, null=True, - help_text="Logo shown in the site header and footer. Leave empty to use the default static icon.", + verbose_name="Header and footer logo", + help_text="Brand logo shown in the site navigation and footer. Leave empty to use the default logo.", ) icon_alt = models.CharField( max_length=200, blank=True, help_text="Alt text for the brand icon (decorative icons can stay empty).", ) + website_icon = models.FileField( + upload_to="branding/icons/", + blank=True, + null=True, + validators=[ + FileExtensionValidator( + allowed_extensions=("ico", "png", "svg", "jpg", "jpeg", "webp") + ) + ], + help_text=( + "Icon shown in browser tabs and bookmarks. Use a square ICO, PNG, SVG, " + "JPG, or WebP file. Leave empty to use the default Radiuma icons." + ), + ) + hero_logo = models.ImageField( + upload_to="branding/hero/", + blank=True, + null=True, + help_text=( + "Large brand artwork shown in the homepage hero. Leave empty to use " + "the existing hero image." + ), + ) + hero_logo_alt = models.CharField( + max_length=200, + blank=True, + default="Radiuma", + help_text="Accessible description for the homepage hero logo.", + ) navbar_icon_size = models.PositiveSmallIntegerField( default=26, - help_text="Width and height in pixels for the header icon.", + help_text="Maximum height in pixels for the header logo. Width scales automatically.", ) footer_icon_size = models.PositiveSmallIntegerField( default=34, - help_text="Width and height in pixels for the footer icon.", + help_text="Maximum height in pixels for the footer logo. Width scales automatically.", ) show_border = models.BooleanField( default=False, @@ -61,9 +92,9 @@ class SiteBranding(models.Model): def icon_style(self, size_px): parts = [ - f"width:{size_px}px", + "width:auto", f"height:{size_px}px", - f"object-fit:{self.object_fit}", + "object-fit:contain", ] if self.show_border: parts.append(f"border:{self.border_width}px solid {self.border_color}") diff --git a/apps/core/tests/test_site_branding.py b/apps/core/tests/test_site_branding.py index b2eed72..4e16cd3 100644 --- a/apps/core/tests/test_site_branding.py +++ b/apps/core/tests/test_site_branding.py @@ -1,4 +1,5 @@ from django.test import RequestFactory, TestCase +from django.urls import reverse from apps.core.context_processors import site_branding from apps.core.models import SiteBranding @@ -17,7 +18,9 @@ class SiteBrandingModelTests(TestCase): branding.border_width = 2 branding.navbar_icon_size = 30 style = branding.navbar_icon_style - self.assertIn("width:30px", style) + self.assertIn("width:auto", style) + self.assertIn("height:30px", style) + self.assertIn("object-fit:contain", style) self.assertIn("border:2px solid #ffffff", style) def test_icon_style_omits_border_when_disabled(self): @@ -33,3 +36,26 @@ class SiteBrandingContextProcessorTests(TestCase): ctx = site_branding(request) self.assertIn("site_branding", ctx) self.assertIsInstance(ctx["site_branding"], SiteBranding) + + +class SiteBrandingTemplateTests(TestCase): + def test_custom_brand_assets_are_rendered_independently(self): + branding = SiteBranding.load() + branding.icon = "branding/navigation-logo.png" + branding.website_icon = "branding/icons/site-icon.png" + branding.hero_logo = "branding/hero/hero-logo.png" + branding.hero_logo_alt = "Radiuma research platform" + branding.save() + + response = self.client.get(reverse("pages:home")) + + self.assertContains(response, 'src="/media/branding/navigation-logo.png"') + self.assertContains(response, 'href="/media/branding/icons/site-icon.png"') + self.assertContains(response, 'src="/media/branding/hero/hero-logo.png"') + self.assertContains(response, 'alt="Radiuma research platform"') + + def test_default_assets_remain_when_custom_assets_are_empty(self): + response = self.client.get(reverse("pages:home")) + + self.assertContains(response, "images/favicon-16.png") + self.assertContains(response, "images/screenshot-1.jpg") diff --git a/apps/pages/tests/test_video.py b/apps/pages/tests/test_video.py index 35acd90..601df72 100644 --- a/apps/pages/tests/test_video.py +++ b/apps/pages/tests/test_video.py @@ -80,6 +80,17 @@ class CustomPageVideoSectionTest(TestCase): class PageVideoTest(TestCase): + def _create_page_videos(self, page, count): + for index in range(count): + PageVideo.objects.create( + page=page, + title=f"Video {index + 1}", + video_source="youtube", + video_url="https://youtu.be/dQw4w9WgXcQ", + order=index, + is_active=True, + ) + def test_contact_page_video(self): PageVideo.objects.create( page=PageVideo.PAGE_CONTACT, @@ -103,6 +114,44 @@ class PageVideoTest(TestCase): response = self.client.get(reverse("pages:faq")) self.assertContains(response, "Tutorial") + def test_page_shows_three_video_preview_and_archive_link(self): + self._create_page_videos(PageVideo.PAGE_FAQ, 8) + + response = self.client.get(reverse("pages:faq")) + + self.assertEqual(response.content.count(b"youtube-nocookie.com/embed"), 3) + self.assertContains( + response, + reverse( + "pages:video_archive", + kwargs={"library": PageVideo.PAGE_FAQ}, + ), + ) + self.assertContains(response, "More videos") + + def test_page_video_archive_is_paginated(self): + self._create_page_videos(PageVideo.PAGE_FAQ, 8) + archive_url = reverse( + "pages:video_archive", + kwargs={"library": PageVideo.PAGE_FAQ}, + ) + + first_page = self.client.get(archive_url) + second_page = self.client.get(archive_url, {"page": 2}) + + self.assertEqual(first_page.status_code, 200) + self.assertEqual(first_page.content.count(b"youtube-nocookie.com/embed"), 6) + self.assertContains(first_page, "Page 1 of 2") + self.assertContains(first_page, "?page=2") + self.assertEqual(second_page.content.count(b"youtube-nocookie.com/embed"), 2) + self.assertContains(second_page, "Page 2 of 2") + + def test_unknown_page_video_archive_returns_404(self): + response = self.client.get( + reverse("pages:video_archive", kwargs={"library": "unknown"}) + ) + self.assertEqual(response.status_code, 404) + class ProductVideoTest(TestCase): def setUp(self): @@ -129,6 +178,30 @@ class ProductVideoTest(TestCase): self.assertContains(response, "Product Demo") self.assertContains(response, "video-block--full") + def test_product_video_preview_links_to_paginated_archive(self): + for index in range(7): + ProductVideo.objects.create( + main_product=self.main_product, + title=f"Product video {index + 1}", + video_source="youtube", + video_url="https://youtu.be/dQw4w9WgXcQ", + order=index, + is_active=True, + ) + archive_url = reverse( + "products:main_product_videos", + kwargs={"main_slug": self.main_product.slug}, + ) + + detail_response = self.client.get(self.main_product.get_absolute_url()) + archive_response = self.client.get(archive_url) + second_page = self.client.get(archive_url, {"page": 2}) + + self.assertEqual(detail_response.content.count(b"youtube-nocookie.com/embed"), 3) + self.assertContains(detail_response, archive_url) + self.assertEqual(archive_response.content.count(b"youtube-nocookie.com/embed"), 6) + self.assertEqual(second_page.content.count(b"youtube-nocookie.com/embed"), 1) + class VideoStyledBackgroundTest(TestCase): def test_styled_background_renders_panel(self): diff --git a/apps/pages/urls.py b/apps/pages/urls.py index cf13072..a525aba 100644 --- a/apps/pages/urls.py +++ b/apps/pages/urls.py @@ -9,5 +9,10 @@ urlpatterns = [ path("about/", views.AboutView.as_view(), name="about"), path("faq/", views.FAQView.as_view(), name="faq"), path("contact/", views.ContactView.as_view(), name="contact"), + path( + "videos//", + views.PageVideoArchiveView.as_view(), + name="video_archive", + ), path("/", views.CustomPageView.as_view(), name="custom_page"), ] diff --git a/apps/pages/views.py b/apps/pages/views.py index ef5aab8..26203c7 100644 --- a/apps/pages/views.py +++ b/apps/pages/views.py @@ -1,8 +1,9 @@ import random from django.db.models import Prefetch -from django.http import JsonResponse +from django.http import Http404, JsonResponse from django.shortcuts import render +from django.urls import reverse from django.views import View from django.views.generic import DetailView, ListView, TemplateView @@ -20,6 +21,18 @@ from .models import ( PageVideo, ) +VIDEO_PREVIEW_LIMIT = 3 +VIDEO_ARCHIVE_PAGE_SIZE = 6 + + +def _video_preview_context(queryset, archive_url): + total = queryset.count() + return { + "videos": queryset[:VIDEO_PREVIEW_LIMIT], + "videos_total_count": total, + "videos_archive_url": archive_url if total > VIDEO_PREVIEW_LIMIT else "", + } + def _homepage_products_catalog_queryset(): return ( @@ -74,10 +87,20 @@ class FAQView(ListView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["faq_videos"] = PageVideo.objects.filter( + videos = PageVideo.objects.filter( page=PageVideo.PAGE_FAQ, is_active=True, - ).order_by("order") + ).order_by("order", "pk") + preview = _video_preview_context( + videos, + reverse( + "pages:video_archive", + kwargs={"library": PageVideo.PAGE_FAQ}, + ), + ) + ctx["faq_videos"] = preview["videos"] + ctx["videos_total_count"] = preview["videos_total_count"] + ctx["videos_archive_url"] = preview["videos_archive_url"] return ctx @@ -90,16 +113,26 @@ class ContactView(View): return f"{a} + {b}" def get(self, request, *args, **kwargs): + videos = PageVideo.objects.filter( + page=PageVideo.PAGE_CONTACT, + is_active=True, + ).order_by("order", "pk") + preview = _video_preview_context( + videos, + reverse( + "pages:video_archive", + kwargs={"library": PageVideo.PAGE_CONTACT}, + ), + ) return render( request, self.template_name, { "form": ContactForm(), "captcha_question": self._new_captcha(request), - "contact_videos": PageVideo.objects.filter( - page=PageVideo.PAGE_CONTACT, - is_active=True, - ).order_by("order"), + "contact_videos": preview["videos"], + "videos_total_count": preview["videos_total_count"], + "videos_archive_url": preview["videos_archive_url"], }, ) @@ -163,3 +196,52 @@ class CustomPageView(DetailView): ) ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset() return ctx + + +class PageVideoArchiveView(ListView): + model = PageVideo + template_name = "videos/archive.html" + context_object_name = "videos" + paginate_by = VIDEO_ARCHIVE_PAGE_SIZE + + PAGE_CONFIG = { + PageVideo.PAGE_CONTACT: ( + "Contact videos", + "Guides and updates from the Radiuma team.", + "pages:contact", + ), + PageVideo.PAGE_FAQ: ( + "FAQ videos", + "Video answers to common questions.", + "pages:faq", + ), + } + + def get_page_config(self): + try: + return self.PAGE_CONFIG[self.kwargs["library"]] + except KeyError as exc: + raise Http404("Video library not found.") from exc + + def get_queryset(self): + self.get_page_config() + return PageVideo.objects.filter( + page=self.kwargs["library"], + is_active=True, + ).order_by("order", "pk") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + title, description, source_url_name = self.get_page_config() + context.update( + { + "library_title": title, + "library_description": description, + "library_back_url": reverse(source_url_name), + "library_back_label": "Back to page", + "pagination_range": context["paginator"].get_elided_page_range( + context["page_obj"].number + ), + } + ) + return context diff --git a/apps/products/urls.py b/apps/products/urls.py index e94b4c2..74ac644 100644 --- a/apps/products/urls.py +++ b/apps/products/urls.py @@ -11,6 +11,16 @@ urlpatterns = [ views.ReleaseAssetDownloadView.as_view(), name="release_asset_download", ), + path( + "/videos/", + views.ProductVideoArchiveView.as_view(), + name="main_product_videos", + ), + path( + "//videos/", + views.ProductVideoArchiveView.as_view(), + name="sub_product_videos", + ), path( "/", views.MainProductDetailView.as_view(), diff --git a/apps/products/views.py b/apps/products/views.py index ec7e52f..cfc6201 100644 --- a/apps/products/views.py +++ b/apps/products/views.py @@ -2,6 +2,7 @@ import mimetypes from django.http import FileResponse, Http404 from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.views import View from django.views.generic import DetailView, ListView, TemplateView @@ -9,6 +10,18 @@ from .models import Article, ArticleCitation, ArticleSection, MainProduct, Produ from .release_assets import FILE_FIELD_BY_ASSET_KEY, RELEASE_ASSET_KEYS, URL_FIELD_BY_ASSET_KEY from .release_context import build_archive_context, build_release_context +VIDEO_PREVIEW_LIMIT = 3 +VIDEO_ARCHIVE_PAGE_SIZE = 6 + + +def _product_video_preview(queryset, archive_url): + total = queryset.count() + return { + "product_videos": queryset[:VIDEO_PREVIEW_LIMIT], + "videos_total_count": total, + "videos_archive_url": archive_url if total > VIDEO_PREVIEW_LIMIT else "", + } + class ReleaseAssetDownloadView(View): def get(self, request, version_id, asset): @@ -58,7 +71,16 @@ class MainProductDetailView(DetailView): context["articles"] = self.object.articles.prefetch_related( "sections", "citations" ).all() - context["product_videos"] = self.object.videos.filter(is_active=True).order_by("order") + videos = self.object.videos.filter(is_active=True).order_by("order", "pk") + context.update( + _product_video_preview( + videos, + reverse( + "products:main_product_videos", + kwargs={"main_slug": self.object.slug}, + ), + ) + ) release_context = build_release_context( self.object.distribution, self.object.versions.filter(is_active=True), @@ -112,7 +134,19 @@ class SubProductDetailView(TemplateView): context["articles"] = sub_product.articles.prefetch_related( "sections", "citations" ).all() - context["product_videos"] = sub_product.videos.filter(is_active=True).order_by("order") + videos = sub_product.videos.filter(is_active=True).order_by("order", "pk") + context.update( + _product_video_preview( + videos, + reverse( + "products:sub_product_videos", + kwargs={ + "main_slug": main_product.slug, + "sub_slug": sub_product.slug, + }, + ), + ) + ) context["siblings"] = ( SubProduct.objects.filter(main_product=main_product, is_active=True) .exclude(pk=sub_product.pk) @@ -127,6 +161,50 @@ class SubProductDetailView(TemplateView): return context +class ProductVideoArchiveView(ListView): + model = ProductVideo + template_name = "videos/archive.html" + context_object_name = "videos" + paginate_by = VIDEO_ARCHIVE_PAGE_SIZE + + def get_parent(self): + main_product = get_object_or_404( + MainProduct, + slug=self.kwargs["main_slug"], + is_active=True, + ) + sub_slug = self.kwargs.get("sub_slug") + if sub_slug: + return get_object_or_404( + SubProduct, + slug=sub_slug, + main_product=main_product, + is_active=True, + ) + return main_product + + def get_queryset(self): + self.parent_object = self.get_parent() + return self.parent_object.videos.filter(is_active=True).order_by("order", "pk") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update( + { + "library_title": f"{self.parent_object.name} videos", + "library_description": ( + "Tutorials, demonstrations, and technical guides." + ), + "library_back_url": self.parent_object.get_absolute_url(), + "library_back_label": f"Back to {self.parent_object.name}", + "pagination_range": context["paginator"].get_elided_page_range( + context["page_obj"].number + ), + } + ) + return context + + class SubProductOlderVersionsView(TemplateView): template_name = "products/versions_archive.html" diff --git a/static/css/main.css b/static/css/main.css index 9a42baf..dab15d3 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -3702,6 +3702,53 @@ a.citation-count-badge:hover { margin-top: 0; } +.video-preview-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.video-preview-heading .section-badge { margin-bottom: 0.55rem; } +.video-preview-heading h2 { font-size: clamp(1.4rem, 3vw, 2rem); } +.video-preview-heading > span { color: var(--text-muted); font-size: 0.76rem; font-weight: 700; } +.video-preview-more { display: flex; justify-content: center; margin-top: 1.75rem; } + +.video-library-hero .page-hero-content { max-width: none; } +.page-hero-content--row { display: flex; align-items: flex-end; justify-content: space-between; gap: 2rem; } +.video-library-section { background: var(--bg-secondary); } +.video-library-summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; color: var(--text-secondary); font-size: 0.8rem; } +.video-library-summary strong { color: var(--text-primary); font-size: 1rem; } +.video-library-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; } +.video-library-card { min-width: 0; padding: 1.2rem; border: 1px solid var(--glass-border); border-radius: var(--radius-md); background: var(--glass-bg); box-shadow: var(--glass-shadow); } +.video-library-card .video-panel--styled { height: 100%; padding: 1.25rem; } +.video-library-card .video-panel-title, +.video-library-card .section-title { font-size: 1.05rem; } +.video-library-card .section-header { margin-bottom: 1rem; text-align: left; } +.video-library-card .section-desc { margin-bottom: 1rem; font-size: 0.78rem; } +.video-library-empty { grid-column: 1 / -1; } + +.video-pagination { display: grid; grid-template-columns: minmax(110px, 1fr) auto minmax(110px, 1fr); align-items: center; gap: 1rem; margin-top: 2.25rem; padding-top: 1.5rem; border-top: 1px solid var(--glass-border); } +.video-pagination__pages { display: flex; align-items: center; gap: 0.35rem; } +.video-pagination__page, +.video-pagination__ellipsis { display: grid; place-items: center; min-width: 38px; height: 38px; padding: 0 0.4rem; border: 1px solid var(--glass-border); border-radius: var(--radius-sm); background: var(--glass-bg); color: var(--text-secondary); font-size: 0.76rem; font-weight: 700; } +.video-pagination__page:hover { border-color: var(--accent-blue); color: var(--accent-blue-light); } +.video-pagination__page.is-current { border-color: var(--accent-blue); background: var(--accent-blue); color: var(--text-primary); } +.video-pagination__ellipsis { border-color: transparent; background: transparent; } +.video-pagination__direction { color: var(--accent-blue-light); font-size: 0.78rem; font-weight: 700; } +.video-pagination__direction:last-child { justify-self: end; } +.video-pagination__direction.is-disabled { color: var(--text-muted); pointer-events: none; } + +@media (max-width: 768px) { + .video-library-grid { grid-template-columns: 1fr; } + .video-pagination { grid-template-columns: 1fr 1fr; } + .video-pagination__pages { grid-column: 1 / -1; grid-row: 1; justify-content: center; flex-wrap: wrap; } + .video-pagination__direction { grid-row: 2; } + .video-preview-heading, + .page-hero-content--row { align-items: flex-start; flex-direction: column; } +} + @media (max-width: 640px) { .video-panel--styled { padding: 1.25rem 1rem 1rem; diff --git a/templates/base.html b/templates/base.html index ffc5390..aebed31 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,9 +6,14 @@ {% block title %}Radiuma{% endblock %} | Radiuma + {% if site_branding.website_icon %} + + + {% else %} + {% endif %} diff --git a/templates/pages/home.html b/templates/pages/home.html index 3ab8566..477d6e9 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -35,10 +35,12 @@ {% endif %} {% endif %} - {% if hero and hero.image or not hero %} + {% if site_branding.hero_logo or hero and hero.image or not hero %}
- {% if hero and hero.image %} + {% if site_branding.hero_logo %} + {{ site_branding.hero_logo_alt|default:'Radiuma' }} + {% elif hero and hero.image %} {{ hero.image_alt }} {% elif not hero %} Radiuma application — main workflow view diff --git a/templates/partials/_brand_icon.html b/templates/partials/_brand_icon.html index 53dd856..e32da22 100644 --- a/templates/partials/_brand_icon.html +++ b/templates/partials/_brand_icon.html @@ -4,8 +4,6 @@ {% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/favicon-180.png' %}"{% endif %} {% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %} class="brand-icon" - width="{{ site_branding.footer_icon_size }}" - height="{{ site_branding.footer_icon_size }}" style="{{ site_branding.footer_icon_style }}" /> {% else %} @@ -13,8 +11,6 @@ {% if site_branding.icon %}src="{{ site_branding.icon.url }}"{% else %}src="{% static 'images/favicon-180.png' %}"{% endif %} {% if site_branding.icon_alt %}alt="{{ site_branding.icon_alt }}"{% else %}alt="" aria-hidden="true"{% endif %} class="brand-icon" - width="{{ site_branding.navbar_icon_size }}" - height="{{ site_branding.navbar_icon_size }}" style="{{ site_branding.navbar_icon_style }}" /> {% endif %} diff --git a/templates/partials/_page_videos.html b/templates/partials/_page_videos.html index 9a6be3e..0b2c46a 100644 --- a/templates/partials/_page_videos.html +++ b/templates/partials/_page_videos.html @@ -1,6 +1,12 @@ {% if videos %} -
+
+ {% if videos_archive_url %} +
+
Video library

Featured videos

+ {{ videos_total_count }} videos +
+ {% endif %}
{% for video in videos %} {% if video.has_video %} @@ -10,6 +16,11 @@ {% endif %} {% endfor %}
+ {% if videos_archive_url %} + + {% endif %}
{% endif %} diff --git a/templates/videos/archive.html b/templates/videos/archive.html new file mode 100644 index 0000000..1567b8c --- /dev/null +++ b/templates/videos/archive.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block title %}{{ library_title }}{% endblock %} +{% block meta_description %}Browse {{ library_title|lower }} from Radiuma.{% endblock %} + +{% block content %} +
+
+
+
+
Video library
+

{{ library_title }}

+

{{ library_description }}

+
+ ← {{ library_back_label }} +
+
+
+ +
+
+
+

{{ paginator.count }} video{{ paginator.count|pluralize }}

+ {% if paginator.num_pages > 1 %}Page {{ page_obj.number }} of {{ paginator.num_pages }}{% endif %} +
+ +
+ {% for video in videos %} +
+ {% include "partials/_video_block.html" with video=video compact_header=True %} +
+ {% empty %} +

No videos available

New videos will appear here when they are published.

+ {% endfor %} +
+ + {% if page_obj.has_other_pages %} + + {% endif %} +
+
+{% endblock %}