fix: show product in replacement of sub products in homepage

This commit is contained in:
mohamad
2026-05-26 17:38:53 +03:30
parent cacf1ba77b
commit 52516d5a5a
17 changed files with 402 additions and 205 deletions
@@ -21,6 +21,8 @@ MAIN_PRODUCTS = [
"(IBSI 1.0), and implements image filters standardized against IBSI 2.0." "(IBSI 1.0), and implements image filters standardized against IBSI 2.0."
), ),
"order": 1, "order": 1,
"show_on_homepage": True,
"homepage_order": 1,
"sub_products": [ "sub_products": [
{ {
"name": "Image Processing", "name": "Image Processing",
@@ -34,8 +36,6 @@ MAIN_PRODUCTS = [
"for consistent, reproducible research outcomes." "for consistent, reproducible research outcomes."
), ),
"order": 1, "order": 1,
"show_on_homepage": True,
"homepage_order": 1,
"articles": [ "articles": [
{ {
"title": "Image Filtering Techniques", "title": "Image Filtering Techniques",
@@ -408,10 +408,6 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("Content seeded successfully.")) self.stdout.write(self.style.SUCCESS("Content seeded successfully."))
def _seed_products(self): def _seed_products(self):
SubProduct.objects.filter(show_on_homepage=False, homepage_order=0).update(
show_on_homepage=True,
)
for product_data in MAIN_PRODUCTS: for product_data in MAIN_PRODUCTS:
sub_products_data = product_data.pop("sub_products") sub_products_data = product_data.pop("sub_products")
main_product, created = MainProduct.objects.get_or_create( main_product, created = MainProduct.objects.get_or_create(
+64 -15
View File
@@ -46,13 +46,13 @@ class HeroSectionAdmin(admin.ModelAdmin):
class HomepageSectionItemInline(admin.TabularInline): class HomepageSectionItemInline(admin.TabularInline):
model = HomepageSectionItem model = HomepageSectionItem
extra = 1 extra = 1
fields = ("icon", "title", "content", "image", "order") fields = ("icon", "title", "content", "url", "image", "order")
ordering = ("order",) ordering = ("order",)
@admin.register(HomepageSection) @admin.register(HomepageSection)
class HomepageSectionAdmin(admin.ModelAdmin): class HomepageSectionAdmin(admin.ModelAdmin):
list_display = ("section_type", "badge", "title", "order", "is_active") list_display = ("section_type", "title", "badge", "order", "is_active")
list_filter = ("section_type", "is_active") list_filter = ("section_type", "is_active")
list_editable = ("order", "is_active") list_editable = ("order", "is_active")
inlines = [HomepageSectionItemInline] inlines = [HomepageSectionItemInline]
@@ -147,22 +147,33 @@ class CustomPageSectionInline(admin.StackedInline):
show_change_link = True show_change_link = True
@admin.register(CustomPage) class CustomPageSectionItemOnPageInline(admin.TabularInline):
class CustomPageAdmin(admin.ModelAdmin): model = CustomPageSectionItem
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at") fk_name = "page"
list_filter = ("show_in_nav", "is_published") extra = 1
search_fields = ("title", "slug", "menu_label") verbose_name = "Section item"
list_editable = ("show_in_nav", "menu_order", "is_published") verbose_name_plural = "Section items (subsections)"
prepopulated_fields = {"slug": ("title",)} fields = (
inlines = [CustomPageSectionInline] "section",
fieldsets = ( "icon",
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}), "badge",
("Navigation", {"fields": ("show_in_nav", "menu_order")}), "title",
("Publishing", {"fields": ("is_published",)}), "content_format",
"content",
"url",
"image",
"order",
) )
ordering = ("section", "order")
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "section":
page_id = request.resolver_match.kwargs.get("object_id") if request.resolver_match else None
if page_id:
kwargs["queryset"] = CustomPageSection.objects.filter(page_id=page_id).order_by("order")
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(CustomPageSection)
class CustomPageSectionAdmin(admin.ModelAdmin): class CustomPageSectionAdmin(admin.ModelAdmin):
list_display = ("page", "section_type", "title", "order", "is_active") list_display = ("page", "section_type", "title", "order", "is_active")
list_filter = ("section_type", "is_active", "page") list_filter = ("section_type", "is_active", "page")
@@ -175,6 +186,44 @@ class CustomPageSectionAdmin(admin.ModelAdmin):
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
) )
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
if isinstance(instance, CustomPageSectionItem):
instance.page = form.instance.page
instance.save()
for obj in formset.deleted_objects:
obj.delete()
formset.save_m2m()
@admin.register(CustomPage)
class CustomPageAdmin(admin.ModelAdmin):
list_display = ("title", "slug", "show_in_nav", "menu_order", "is_published", "updated_at")
list_filter = ("show_in_nav", "is_published")
search_fields = ("title", "slug", "menu_label")
list_editable = ("show_in_nav", "menu_order", "is_published")
prepopulated_fields = {"slug": ("title",)}
inlines = [CustomPageSectionInline, CustomPageSectionItemOnPageInline]
fieldsets = (
(None, {"fields": ("title", "slug", "menu_label", "meta_description")}),
("Navigation", {"fields": ("show_in_nav", "menu_order")}),
("Publishing", {"fields": ("is_published",)}),
)
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
if isinstance(instance, CustomPageSectionItem):
instance.page = form.instance
instance.save()
for obj in formset.deleted_objects:
obj.delete()
formset.save_m2m()
admin.site.register(CustomPageSection, CustomPageSectionAdmin)
@admin.register(DownloadItem) @admin.register(DownloadItem)
class DownloadItemAdmin(admin.ModelAdmin): class DownloadItemAdmin(admin.ModelAdmin):
@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2026-05-26 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0008_custom_pages'),
]
operations = [
migrations.AlterField(
model_name='homepagesection',
name='section_type',
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.0.2 on 2026-05-26 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0009_alter_homepagesection_section_type'),
]
operations = [
migrations.AddField(
model_name='homepagesectionitem',
name='url',
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
),
migrations.AlterField(
model_name='custompagesection',
name='section_type',
field=models.CharField(choices=[('hero', 'Hero'), ('intro', 'Intro Card'), ('grid', 'Grid Cards'), ('history', 'History Block'), ('custom', 'Custom Content'), ('features', 'Features Grid'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products Grid'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('supporters', 'Supporters'), ('about_strip', 'About Strip'), ('faq', 'FAQ Accordion')], default='custom', max_length=30),
),
migrations.AlterField(
model_name='custompagesectionitem',
name='url',
field=models.CharField(blank=True, help_text='Optional link; makes this item clickable.', max_length=300),
),
migrations.AlterField(
model_name='homepagesection',
name='section_type',
field=models.CharField(choices=[('features', 'Features'), ('screenshots', 'Screenshots Gallery'), ('products', 'Products'), ('products_catalog', 'Products with Sub-products'), ('problems', 'Problems / Value Proposition'), ('about_strip', 'About Strip'), ('supporters', 'Supporters')], max_length=30),
),
]
@@ -0,0 +1,48 @@
from django.db import migrations, models
import django.db.models.deletion
def set_custom_page_section_item_page(apps, schema_editor):
CustomPageSectionItem = apps.get_model("pages", "CustomPageSectionItem")
for item in CustomPageSectionItem.objects.select_related("section").iterator():
item.page_id = item.section.page_id
item.save(update_fields=["page_id"])
class Migration(migrations.Migration):
dependencies = [
("pages", "0010_homepagesectionitem_url_and_more"),
]
operations = [
migrations.AlterField(
model_name="aboutsectionitem",
name="url",
field=models.CharField(
blank=True,
help_text="Optional link; makes this item clickable.",
max_length=300,
),
),
migrations.AddField(
model_name="custompagesectionitem",
name="page",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="section_items",
to="pages.custompage",
),
),
migrations.RunPython(set_custom_page_section_item_page, migrations.RunPython.noop),
migrations.AlterField(
model_name="custompagesectionitem",
name="page",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="section_items",
to="pages.custompage",
),
),
]
+18 -3
View File
@@ -41,6 +41,7 @@ class HomepageSection(models.Model):
TYPE_FEATURES = "features" TYPE_FEATURES = "features"
TYPE_SCREENSHOTS = "screenshots" TYPE_SCREENSHOTS = "screenshots"
TYPE_PRODUCTS = "products" TYPE_PRODUCTS = "products"
TYPE_PRODUCTS_CATALOG = "products_catalog"
TYPE_PROBLEMS = "problems" TYPE_PROBLEMS = "problems"
TYPE_ABOUT_STRIP = "about_strip" TYPE_ABOUT_STRIP = "about_strip"
TYPE_SUPPORTERS = "supporters" TYPE_SUPPORTERS = "supporters"
@@ -49,12 +50,13 @@ class HomepageSection(models.Model):
(TYPE_FEATURES, "Features"), (TYPE_FEATURES, "Features"),
(TYPE_SCREENSHOTS, "Screenshots Gallery"), (TYPE_SCREENSHOTS, "Screenshots Gallery"),
(TYPE_PRODUCTS, "Products"), (TYPE_PRODUCTS, "Products"),
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
(TYPE_PROBLEMS, "Problems / Value Proposition"), (TYPE_PROBLEMS, "Problems / Value Proposition"),
(TYPE_ABOUT_STRIP, "About Strip"), (TYPE_ABOUT_STRIP, "About Strip"),
(TYPE_SUPPORTERS, "Supporters"), (TYPE_SUPPORTERS, "Supporters"),
] ]
section_type = models.CharField(max_length=30, choices=TYPE_CHOICES, unique=True) section_type = models.CharField(max_length=30, choices=TYPE_CHOICES)
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
description = models.TextField(blank=True) description = models.TextField(blank=True)
@@ -77,6 +79,7 @@ class HomepageSectionItem(models.Model):
icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).") icon = models.CharField(max_length=20, blank=True, help_text="Emoji or short symbol (e.g. ⚗️).")
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True) content = models.TextField(blank=True)
url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).") image = models.ImageField(upload_to="homepage/items/", blank=True, null=True, help_text="Logo or image (used for Supporters cards).")
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
@@ -134,7 +137,7 @@ class AboutSectionItem(models.Model):
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True, help_text="Description text or link label for History links.") content = models.TextField(blank=True, help_text="Description text or link label for History links.")
url = models.URLField(blank=True, help_text="Used for History block links.") url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(upload_to="about/", blank=True, null=True) image = models.ImageField(upload_to="about/", blank=True, null=True)
image_alt = models.CharField(max_length=200, blank=True) image_alt = models.CharField(max_length=200, blank=True)
is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).") is_featured = models.BooleanField(default=False, help_text="Mark as featured item (e.g. large screenshot).")
@@ -267,6 +270,7 @@ class CustomPageSection(models.Model):
TYPE_FEATURES = "features" TYPE_FEATURES = "features"
TYPE_SCREENSHOTS = "screenshots" TYPE_SCREENSHOTS = "screenshots"
TYPE_PRODUCTS = "products" TYPE_PRODUCTS = "products"
TYPE_PRODUCTS_CATALOG = "products_catalog"
TYPE_PROBLEMS = "problems" TYPE_PROBLEMS = "problems"
TYPE_SUPPORTERS = "supporters" TYPE_SUPPORTERS = "supporters"
TYPE_ABOUT_STRIP = "about_strip" TYPE_ABOUT_STRIP = "about_strip"
@@ -281,6 +285,7 @@ class CustomPageSection(models.Model):
(TYPE_FEATURES, "Features Grid"), (TYPE_FEATURES, "Features Grid"),
(TYPE_SCREENSHOTS, "Screenshots Gallery"), (TYPE_SCREENSHOTS, "Screenshots Gallery"),
(TYPE_PRODUCTS, "Products Grid"), (TYPE_PRODUCTS, "Products Grid"),
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
(TYPE_PROBLEMS, "Problems / Value Proposition"), (TYPE_PROBLEMS, "Problems / Value Proposition"),
(TYPE_SUPPORTERS, "Supporters"), (TYPE_SUPPORTERS, "Supporters"),
(TYPE_ABOUT_STRIP, "About Strip"), (TYPE_ABOUT_STRIP, "About Strip"),
@@ -315,13 +320,18 @@ class CustomPageSection(models.Model):
class CustomPageSectionItem(models.Model): class CustomPageSectionItem(models.Model):
page = models.ForeignKey(
CustomPage,
on_delete=models.CASCADE,
related_name="section_items",
)
section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items") section = models.ForeignKey(CustomPageSection, on_delete=models.CASCADE, related_name="items")
icon = models.CharField(max_length=20, blank=True) icon = models.CharField(max_length=20, blank=True)
badge = models.CharField(max_length=100, blank=True) badge = models.CharField(max_length=100, blank=True)
title = models.CharField(max_length=300, blank=True) title = models.CharField(max_length=300, blank=True)
content = models.TextField(blank=True) content = models.TextField(blank=True)
content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN) content_format = models.CharField(max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN)
url = models.URLField(blank=True) url = models.CharField(max_length=300, blank=True, help_text="Optional link; makes this item clickable.")
image = models.ImageField(upload_to="pages/custom/", blank=True, null=True) image = models.ImageField(upload_to="pages/custom/", blank=True, null=True)
image_alt = models.CharField(max_length=200, blank=True) image_alt = models.CharField(max_length=200, blank=True)
is_featured = models.BooleanField(default=False) is_featured = models.BooleanField(default=False)
@@ -336,5 +346,10 @@ class CustomPageSectionItem(models.Model):
def rendered_content(self): def rendered_content(self):
return render_content(self.content, self.content_format) return render_content(self.content, self.content_format)
def save(self, *args, **kwargs):
if self.section_id:
self.page_id = self.section.page_id
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return f"{self.section} {self.title or self.icon or '(item)'}" return f"{self.section} {self.title or self.icon or '(item)'}"
+21 -1
View File
@@ -1,7 +1,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from apps.pages.models import CustomPage, CustomPageSection from apps.pages.models import CustomPage, CustomPageSection, CustomPageSectionItem
class CustomPageViewTest(TestCase): class CustomPageViewTest(TestCase):
@@ -40,6 +40,26 @@ class CustomPageViewTest(TestCase):
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"})) response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "missing"}))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_section_item_with_url_renders_as_link(self):
section = CustomPageSection.objects.create(
page=self.page,
section_type=CustomPageSection.TYPE_FEATURES,
title="Highlights",
is_active=True,
)
CustomPageSectionItem.objects.create(
page=self.page,
section=section,
title="Documentation",
content="Read the docs.",
url="/about/",
order=1,
)
response = self.client.get(reverse("pages:custom_page", kwargs={"slug": "resources"}))
self.assertContains(response, 'href="/about/"')
self.assertContains(response, "section-item-link")
self.assertContains(response, "Documentation")
class CustomPageNavTest(TestCase): class CustomPageNavTest(TestCase):
def test_nav_custom_pages_in_context(self): def test_nav_custom_pages_in_context(self):
+21 -7
View File
@@ -1,11 +1,12 @@
import random import random
from django.db.models import Prefetch
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from django.views.generic import DetailView, ListView, TemplateView from django.views.generic import DetailView, ListView, TemplateView
from apps.products.models import SubProduct from apps.products.models import MainProduct, SubProduct
from .forms import ContactForm from .forms import ContactForm
from .models import ( from .models import (
@@ -18,6 +19,19 @@ from .models import (
) )
def _homepage_products_catalog_queryset():
return (
MainProduct.objects.filter(is_active=True)
.prefetch_related(
Prefetch(
"sub_products",
queryset=SubProduct.objects.filter(is_active=True).order_by("order", "name"),
)
)
.order_by("order", "name")
)
class HomeView(TemplateView): class HomeView(TemplateView):
template_name = "pages/home.html" template_name = "pages/home.html"
@@ -29,11 +43,11 @@ class HomeView(TemplateView):
.prefetch_related("items") .prefetch_related("items")
.order_by("order") .order_by("order")
) )
ctx["sub_products"] = ( ctx["homepage_products"] = (
SubProduct.objects.filter(is_active=True, show_on_homepage=True) MainProduct.objects.filter(is_active=True, show_on_homepage=True)
.select_related("main_product")
.order_by("homepage_order", "order") .order_by("homepage_order", "order")
) )
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
return ctx return ctx
@@ -115,9 +129,9 @@ class CustomPageView(DetailView):
.prefetch_related("items") .prefetch_related("items")
.order_by("order") .order_by("order")
) )
ctx["sub_products"] = ( ctx["homepage_products"] = (
SubProduct.objects.filter(is_active=True, show_on_homepage=True) MainProduct.objects.filter(is_active=True, show_on_homepage=True)
.select_related("main_product")
.order_by("homepage_order", "order") .order_by("homepage_order", "order")
) )
ctx["homepage_products_catalog"] = _homepage_products_catalog_queryset()
return ctx return ctx
+4 -3
View File
@@ -76,16 +76,17 @@ class SubProductInline(admin.StackedInline):
@admin.register(MainProduct) @admin.register(MainProduct)
class MainProductAdmin(admin.ModelAdmin): class MainProductAdmin(admin.ModelAdmin):
list_display = ("name", "order", "is_active", "created_at") list_display = ("name", "show_on_homepage", "homepage_order", "order", "is_active", "created_at")
list_filter = ("is_active",) list_filter = ("is_active", "show_on_homepage")
search_fields = ("name", "description") search_fields = ("name", "description")
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
list_editable = ("order", "is_active") list_editable = ("order", "is_active", "show_on_homepage", "homepage_order")
inlines = [MainProductArticleInline, MainProductVersionInline, SubProductInline] inlines = [MainProductArticleInline, MainProductVersionInline, SubProductInline]
fieldsets = ( fieldsets = (
(None, {"fields": ("name", "slug", "short_description", "distribution")}), (None, {"fields": ("name", "slug", "short_description", "distribution")}),
("Description", {"fields": ("description_format", "description")}), ("Description", {"fields": ("description_format", "description")}),
("Media", {"fields": ("image",)}), ("Media", {"fields": ("image",)}),
("Homepage", {"fields": ("show_on_homepage", "homepage_order")}),
("Settings", {"fields": ("order", "is_active")}), ("Settings", {"fields": ("order", "is_active")}),
) )
@@ -0,0 +1,23 @@
# Generated by Django 5.0.2 on 2026-05-26 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0010_release_version_main_product'),
]
operations = [
migrations.AddField(
model_name='mainproduct',
name='homepage_order',
field=models.PositiveIntegerField(default=0, help_text='Order within the homepage Products section.'),
),
migrations.AddField(
model_name='mainproduct',
name='show_on_homepage',
field=models.BooleanField(default=False, help_text='Display this product in the homepage Products section.'),
),
]
@@ -0,0 +1,26 @@
from django.db import migrations
def copy_subproduct_homepage_flags(apps, schema_editor):
SubProduct = apps.get_model("products", "SubProduct")
for sub in (
SubProduct.objects.filter(show_on_homepage=True)
.select_related("main_product")
.order_by("homepage_order", "order")
):
main = sub.main_product
if not main.show_on_homepage:
main.show_on_homepage = True
main.homepage_order = sub.homepage_order
main.save(update_fields=["show_on_homepage", "homepage_order"])
class Migration(migrations.Migration):
dependencies = [
("products", "0011_mainproduct_homepage_order_and_more"),
]
operations = [
migrations.RunPython(copy_subproduct_homepage_flags, migrations.RunPython.noop),
]
+2
View File
@@ -38,6 +38,8 @@ class MainProduct(models.Model):
) )
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
show_on_homepage = models.BooleanField(default=False, help_text="Display this product in the homepage Products section.")
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
+38
View File
@@ -853,6 +853,44 @@ h1, h2, h3, h4, h5, h6 {
line-height: 1.65; line-height: 1.65;
} }
a.section-item-link {
text-decoration: none;
color: inherit;
transition: var(--transition-fast);
}
a.section-item-link:hover {
border-color: rgba(79, 142, 247, 0.35);
}
a.supporter-card.section-item-link {
display: flex;
}
a.screenshot-item.section-item-link {
display: block;
}
a.faq-item--link.section-item-link {
display: block;
padding: 1.25rem 1.5rem;
}
a.faq-item--link .faq-question {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 1rem;
font-weight: 600;
margin: 0;
}
a.faq-item--link .faq-link-preview {
margin-top: 0.75rem;
font-size: 0.88rem;
color: var(--text-secondary);
}
/* ============================================================ /* ============================================================
Products Grid (Home) Products Grid (Home)
============================================================ */ ============================================================ */
+1 -152
View File
@@ -48,157 +48,6 @@
</div> </div>
</section> </section>
{% for section in homepage_sections %} {% include "partials/_page_sections.html" with sections=homepage_sections %}
{% if section.section_type == "features" %}
<section class="section features-section" aria-labelledby="features-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="features-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="features-grid">
{% for item in section.items.all %}
<div class="feature-card glass-card fade-in">
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="feature-title">{{ item.title }}</h3>
<p class="feature-desc" data-readmore="88">{{ item.content }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% elif section.section_type == "screenshots" %}
<section class="section screenshots-section" aria-labelledby="screenshots-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="screenshots-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="screenshots-grid">
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-2.jpg' %}" alt="Radiuma full workflow interface" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-3.png' %}" alt="Radiuma panel view" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-5.png' %}" alt="Radiuma segmentation view" loading="lazy" />
</div>
<div class="screenshot-item fade-in">
<img src="{% static 'images/screenshot-8.jpg' %}" alt="Radiuma radiomics results" loading="lazy" />
</div>
</div>
</div>
</section>
{% elif section.section_type == "products" %}
{% if sub_products %}
<section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="products-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="products-grid">
{% for product in sub_products %}
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
{% if product.image %}
<div class="product-card-image">
<img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
</div>
{% endif %}
<div class="product-card-body">
{% if product.logo %}
<img src="{{ product.logo.url }}" alt="" class="product-card-logo" aria-hidden="true" loading="lazy" />
{% endif %}
<h3 class="product-card-title">{{ product.name }}</h3>
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
<span class="product-card-link">
Explore
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</div>
</a>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{% elif section.section_type == "problems" %}
<section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="problems-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="problems-grid">
{% for item in section.items.all %}
<div class="problem-item glass-card fade-in">
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="problem-title">{{ item.title }}</h3>
<p class="problem-desc" data-readmore="88">{{ item.content }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% elif section.section_type == "supporters" %}
<section class="section supporters-section" aria-labelledby="supporters-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="supporters-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="supporters-grid">
{% for item in section.items.all %}
<div class="supporter-card glass-card fade-in">
{% if item.image %}
<div class="supporter-logo">
<img src="{{ item.image.url }}" alt="{{ item.title }} logo" loading="lazy" />
</div>
{% endif %}
<div class="supporter-body">
<h3 class="supporter-name">{{ item.title }}</h3>
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% elif section.section_type == "about_strip" %}
<section class="section about-strip" aria-labelledby="about-strip-heading-{{ section.pk }}">
<div class="container">
<div class="about-strip-inner glass-card">
<div class="about-strip-content">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
<h2 class="section-title" id="about-strip-heading-{{ section.pk }}">{{ section.title }}</h2>
{% if section.description %}<p class="about-strip-text">{{ section.description }}</p>{% endif %}
{% if section.link_text and section.link_url %}
<a href="{{ section.link_url }}" class="btn-primary">{{ section.link_text }}</a>
{% endif %}
</div>
<div class="about-strip-blobs" aria-hidden="true">
<div class="strip-blob strip-blob--1"></div>
<div class="strip-blob strip-blob--2"></div>
</div>
</div>
</div>
</section>
{% endif %}
{% endfor %}
{% endblock %} {% endblock %}
@@ -0,0 +1,5 @@
{% if url %}
</a>
{% else %}
</div>
{% endif %}
@@ -0,0 +1,5 @@
{% if url %}
<a href="{{ url }}" class="{{ card_class }} glass-card fade-in section-item-link">
{% else %}
<div class="{{ card_class }} glass-card fade-in">
{% endif %}
+73 -18
View File
@@ -48,11 +48,11 @@
{% if items %} {% if items %}
<div class="standards-grid"> <div class="standards-grid">
{% for item in items %} {% for item in items %}
<div class="standard-card glass-card fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="standard-card" %}
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %} {% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %} {% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %} {% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
</div> {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@@ -107,12 +107,11 @@
{% if items %} {% if items %}
<div class="standards-grid" style="margin-top: 1.5rem;"> <div class="standards-grid" style="margin-top: 1.5rem;">
{% for item in items %} {% for item in items %}
<div class="standard-card glass-card fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="standard-card" %}
{% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %} {% if item.badge %}<div class="standard-badge">{{ item.badge }}</div>{% endif %}
{% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %} {% if item.title %}<h3 class="standard-title">{{ item.title }}</h3>{% endif %}
{% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %} {% if item.content %}<p class="standard-desc" data-readmore="88">{{ item.content }}</p>{% endif %}
{% if item.url %}<a href="{{ item.url }}" class="btn-ghost btn--sm" style="margin-top: 0.75rem;" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>{% endif %} {% include "partials/_linked_card_close.html" with url=item.url %}
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@@ -130,11 +129,11 @@
</div> </div>
<div class="features-grid"> <div class="features-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
<div class="feature-card glass-card fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="feature-card" %}
{% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %} {% if item.icon %}<div class="feature-icon" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="feature-title">{{ item.title }}</h3> <h3 class="feature-title">{{ item.title }}</h3>
<p class="feature-desc" data-readmore="88">{{ item.content }}</p> <p class="feature-desc" data-readmore="88">{{ item.content }}</p>
</div> {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -151,9 +150,9 @@
<div class="screenshots-grid"> <div class="screenshots-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
{% if item.image %} {% if item.image %}
<div class="screenshot-item fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="screenshot-item" %}
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }}" loading="lazy" /> <img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }}" loading="lazy" />
</div> {% include "partials/_linked_card_close.html" with url=item.url %}
{% endif %} {% endif %}
{% empty %} {% empty %}
<div class="screenshot-item fade-in"> <div class="screenshot-item fade-in">
@@ -174,7 +173,7 @@
</section> </section>
{% elif section.section_type == "products" %} {% elif section.section_type == "products" %}
{% if sub_products %} {% if homepage_products %}
<section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}"> <section class="section products-section" aria-labelledby="products-heading-{{ section.pk }}">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
@@ -183,7 +182,7 @@
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %} {% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div> </div>
<div class="products-grid"> <div class="products-grid">
{% for product in sub_products %} {% for product in homepage_products %}
<a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in"> <a href="{{ product.get_absolute_url }}" class="product-card glass-card fade-in">
{% if product.image %} {% if product.image %}
<div class="product-card-image"> <div class="product-card-image">
@@ -191,9 +190,6 @@
</div> </div>
{% endif %} {% endif %}
<div class="product-card-body"> <div class="product-card-body">
{% if product.logo %}
<img src="{{ product.logo.url }}" alt="" class="product-card-logo" aria-hidden="true" loading="lazy" />
{% endif %}
<h3 class="product-card-title">{{ product.name }}</h3> <h3 class="product-card-title">{{ product.name }}</h3>
<p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p> <p class="product-card-desc" data-readmore="88">{{ product.short_description }}</p>
<span class="product-card-link"> <span class="product-card-link">
@@ -210,6 +206,53 @@
</section> </section>
{% endif %} {% endif %}
{% elif section.section_type == "products_catalog" %}
{% if homepage_products_catalog %}
<section class="section products-catalog-section" aria-labelledby="products-catalog-heading-{{ section.pk }}">
<div class="container">
<div class="section-header">
{% if section.badge %}<div class="section-badge">{{ section.badge }}</div>{% endif %}
{% if section.title %}<h2 class="section-title" id="products-catalog-heading-{{ section.pk }}">{{ section.title }}</h2>{% endif %}
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
<div class="products-overview-grid">
{% for product in homepage_products_catalog %}
<div class="product-overview-card glass-card fade-in">
{% if product.image %}
<div class="product-overview-image">
<img src="{{ product.image.url }}" alt="{{ product.name }}" loading="lazy" />
</div>
{% endif %}
<div class="product-overview-body">
<h3 class="product-overview-title">{{ product.name }}</h3>
<p class="product-overview-short">{{ product.short_description }}</p>
<p class="product-overview-desc" data-readmore="110">{{ product.description }}</p>
<a href="{{ product.get_absolute_url }}" class="btn-primary">Explore {{ product.name }}</a>
</div>
{% if product.sub_products.all %}
<div class="product-overview-subs">
<h4 class="product-overview-subs-title">Modules</h4>
<ul class="product-overview-subs-list" role="list">
{% for sub in product.sub_products.all %}
<li>
<a href="{{ sub.get_absolute_url }}" class="sub-link-chip">
{{ sub.name }}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2.5 6H9.5M9.5 6L7 3.5M9.5 6L7 8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{% elif section.section_type == "problems" %} {% elif section.section_type == "problems" %}
<section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}"> <section class="section problems-section" aria-labelledby="problems-heading-{{ section.pk }}">
<div class="container"> <div class="container">
@@ -220,11 +263,11 @@
</div> </div>
<div class="problems-grid"> <div class="problems-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
<div class="problem-item glass-card fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="problem-item" %}
{% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %} {% if item.icon %}<div class="problem-number" aria-hidden="true">{{ item.icon }}</div>{% endif %}
<h3 class="problem-title">{{ item.title }}</h3> <h3 class="problem-title">{{ item.title }}</h3>
<p class="problem-desc" data-readmore="88">{{ item.content }}</p> <p class="problem-desc" data-readmore="88">{{ item.content }}</p>
</div> {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -240,7 +283,7 @@
</div> </div>
<div class="supporters-grid"> <div class="supporters-grid">
{% for item in section.items.all %} {% for item in section.items.all %}
<div class="supporter-card glass-card fade-in"> {% include "partials/_linked_card_open.html" with url=item.url card_class="supporter-card" %}
{% if item.image %} {% if item.image %}
<div class="supporter-logo"> <div class="supporter-logo">
<img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }} logo" loading="lazy" /> <img src="{{ item.image.url }}" alt="{{ item.image_alt|default:item.title }} logo" loading="lazy" />
@@ -250,7 +293,7 @@
<h3 class="supporter-name">{{ item.title }}</h3> <h3 class="supporter-name">{{ item.title }}</h3>
{% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %} {% if item.content %}<p class="supporter-desc">{{ item.content }}</p>{% endif %}
</div> </div>
</div> {% include "partials/_linked_card_close.html" with url=item.url %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -289,6 +332,17 @@
{% if items %} {% if items %}
<div class="faq-list"> <div class="faq-list">
{% for item in items %} {% for item in items %}
{% if item.url %}
{% include "partials/_linked_card_open.html" with url=item.url card_class="faq-item faq-item--link" %}
<div class="faq-question">
{{ item.title }}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
{% if item.content %}<div class="rich-content faq-link-preview">{{ item.rendered_content }}</div>{% endif %}
{% include "partials/_linked_card_close.html" with url=item.url %}
{% else %}
<div class="faq-item glass-card fade-in"> <div class="faq-item glass-card fade-in">
<button <button
class="faq-question" class="faq-question"
@@ -311,6 +365,7 @@
<div class="rich-content">{{ item.rendered_content }}</div> <div class="rich-content">{{ item.rendered_content }}</div>
</div> </div>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}