From ac0368c578c6d63436f2fc45941ecd9c63141baa Mon Sep 17 00:00:00 2001 From: maxnoelp2 Date: Mon, 23 Mar 2026 15:21:25 +0100 Subject: [PATCH 1/4] feat: Add MilestoneCard component and update timeline template --- cms_theme/cms_components.py | 87 +++++++++++++++---- .../templates/timeline/milestone_card.html | 30 +++++++ cms_theme/templates/timeline/timeline.html | 4 +- 3 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 cms_theme/templates/timeline/milestone_card.html diff --git a/cms_theme/cms_components.py b/cms_theme/cms_components.py index b071a7a..c84f604 100644 --- a/cms_theme/cms_components.py +++ b/cms_theme/cms_components.py @@ -8,8 +8,6 @@ from djangocms_frontend.component_pool import components from djangocms_frontend.contrib.icon.fields import IconPickerField from djangocms_frontend.contrib.image.fields import ImageFormField - -from .fields import ColorChoiceField from djangocms_frontend.fields import ( ButtonGroup, ColoredButtonGroup, @@ -18,6 +16,8 @@ ) from djangocms_frontend.helpers import first_choice +from .fields import ColorChoiceField + def _hero_clip_path_choices(): """Return (id, label) pairs for the Hero clip_path ChoiceField.""" @@ -142,7 +142,7 @@ class Meta: render_template = "timeline/timeline.html" allow_children = True child_classes = [ - "CardPlugin", + "MilestoneCardPlugin", "TextPlugin", "HeadingPlugin", "SpacingPlugin", @@ -172,6 +172,43 @@ class Meta: ) +@components.register +class MilestoneCard(CMSFrontendComponent): + """Milestone card component for Timeline""" + + class Meta: + name = _("Milestone Card") + render_template = "timeline/milestone_card.html" + allow_children = True + parent_classes = [ + "TimelineContainerPlugin", + ] + mixins = ["Background", "Spacing"] + slots = ( + Slot("links", _("Links"), child_classes=["TextLinkPlugin"]), + Slot("image_top", _("Image Top"), child_classes=["ImagePlugin"]), + Slot("image_bottom", _("Image Bottom"), child_classes=["ImagePlugin"]), + ) + + label = forms.CharField( + label=_("Milestone label"), + required=True, + help_text=_("Short label for the milestone, e.g. a year or date."), + ) + + heading = forms.CharField( + label=_("Milestone heading"), + required=True, + help_text=_("Heading for the milestone card."), + ) + + text = HTMLFormField( + label=_("Milestone text"), + required=False, + help_text=_("Description text for the milestone."), + ) + + @components.register class Footer(CMSFrontendComponent): """Footer component with divider color option""" @@ -992,6 +1029,7 @@ def clean(self): @components.register class CodeBlock(CMSFrontendComponent): """Code card component to render code snippets with syntax highlighting""" + class Media: js = ( "admin/vendor/ace/ace.js" @@ -1098,14 +1136,20 @@ def _fetch_github_stat(self, counter_type): ) resp.raise_for_status() data = resp.json() - return data["stargazers_count" if counter_type == "stars" else "forks_count"] + return data[ + "stargazers_count" if counter_type == "stars" else "forks_count" + ] - since = (datetime.now(tz=timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%d") + since = (datetime.now(tz=timezone.utc) - timedelta(days=30)).strftime( + "%Y-%m-%d" + ) if counter_type == "issues_closed": resp = requests.get( "https://api.github.com/search/issues", - params={"q": f"org:{self.GITHUB_ORG} type:issue is:closed closed:>={since}"}, + params={ + "q": f"org:{self.GITHUB_ORG} type:issue is:closed closed:>={since}" + }, headers={"Accept": "application/vnd.github.v3+json"}, timeout=10, ) @@ -1115,7 +1159,9 @@ def _fetch_github_stat(self, counter_type): if counter_type == "merges": resp = requests.get( "https://api.github.com/search/issues", - params={"q": f"org:{self.GITHUB_ORG} type:pr is:merged merged:>={since}"}, + params={ + "q": f"org:{self.GITHUB_ORG} type:pr is:merged merged:>={since}" + }, headers={"Accept": "application/vnd.github.v3+json"}, timeout=10, ) @@ -1153,16 +1199,19 @@ class Meta: child_classes = ["TextLinkPlugin"] mixins = ["Background", "Attributes"] fieldsets = ( - (None, { - "fields": ( - "icon", - "title", - ("counter_type", "number", "is_percent"), - "number_color", - "description", - "color_style", - ) - }), + ( + None, + { + "fields": ( + "icon", + "title", + ("counter_type", "number", "is_percent"), + "number_color", + "description", + "color_style", + ) + }, + ), ) counter_type = forms.ChoiceField( @@ -1201,4 +1250,6 @@ class Meta: ) def get_short_description(self): - return dict(COUNTER_TYPE_CHOICES).get(self.config.get("counter_type"), _("Manual")) + return dict(COUNTER_TYPE_CHOICES).get( + self.config.get("counter_type"), _("Manual") + ) diff --git a/cms_theme/templates/timeline/milestone_card.html b/cms_theme/templates/timeline/milestone_card.html new file mode 100644 index 0000000..4da25cd --- /dev/null +++ b/cms_theme/templates/timeline/milestone_card.html @@ -0,0 +1,30 @@ +{% load cms_tags frontend %} + +
+
+
+ {% if instance.label %} +

{{ instance.label }}

+ {% endif %} + {% if instance.heading %} +

{{ instance.heading }}

+ {% endif %} +
+ {% for plugin in instance|get_slot:"image_top" %} + {% render_plugin plugin %} + {% endfor %} + {% if instance.text %} +
+ {{ instance.text|safe }} +
+ {% endif %} + {% for plugin in instance|get_slot:"image_top" %} + {% render_plugin plugin %} + {% endfor %} + {% for plugin in instance|get_slot:"links" %} + {% render_plugin plugin %} + {% endfor %} +
+
+
+
\ No newline at end of file diff --git a/cms_theme/templates/timeline/timeline.html b/cms_theme/templates/timeline/timeline.html index f90c761..d80195b 100644 --- a/cms_theme/templates/timeline/timeline.html +++ b/cms_theme/templates/timeline/timeline.html @@ -8,7 +8,7 @@
{# Header content: Heading and intro text #} {% for child in instance.child_plugin_instances %} - {% if child.plugin_type != "CardPlugin" %} + {% if child.plugin_type != "MilestoneCardPlugin" %} {% render_plugin child %} {% endif %} {% endfor %} @@ -16,7 +16,7 @@ {# Timeline items #}
{% for child in instance.child_plugin_instances %} - {% if child.plugin_type == "CardPlugin" and child.child_plugin_instances %} + {% if child.plugin_type == "MilestoneCardPlugin" and child.child_plugin_instances %}
{% render_plugin child %}
From 2470b7cf38291fa47a92867d9371b94c1d84b78e Mon Sep 17 00:00:00 2001 From: maxnoelp2 Date: Mon, 23 Mar 2026 16:03:33 +0100 Subject: [PATCH 2/4] feat: Enhance MilestoneCard with dark background contrast and responsive text adjustments --- backend/static/scss/_timeline.scss | 20 +++++++++++++++++++ cms_theme/cms_components.py | 1 + .../templates/timeline/milestone_card.html | 20 ++++++++++--------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/static/scss/_timeline.scss b/backend/static/scss/_timeline.scss index 49f613f..4b6e05d 100644 --- a/backend/static/scss/_timeline.scss +++ b/backend/static/scss/_timeline.scss @@ -90,6 +90,20 @@ $timeline-circle-size: 15px; } } +// Dark background contrast fix for milestone cards +// Same pattern as features-section: force white text on dark backgrounds +.milestone-card { + .card { + &.bg-dark, + &.bg-secondary, + &.bg-second-primary, + &.bg-dark-green, + &.bg-black { + color: #fff; + } + } +} + // Responsive timeline on mobile devices @include media-breakpoint-down(md) { // Move the timeline ruler to the left @@ -115,3 +129,9 @@ $timeline-circle-size: 15px; left: 0; } } + +.milestone-text { + p { + margin-bottom: 0 !important; + } +} \ No newline at end of file diff --git a/cms_theme/cms_components.py b/cms_theme/cms_components.py index bd6eb03..e80a20a 100644 --- a/cms_theme/cms_components.py +++ b/cms_theme/cms_components.py @@ -189,6 +189,7 @@ class Meta: Slot("image_top", _("Image Top"), child_classes=["ImagePlugin"]), Slot("image_bottom", _("Image Bottom"), child_classes=["ImagePlugin"]), ) + frontend_editable_fields = ("label", "heading", "text") label = forms.CharField( label=_("Milestone label"), diff --git a/cms_theme/templates/timeline/milestone_card.html b/cms_theme/templates/timeline/milestone_card.html index 4da25cd..0e0ffc2 100644 --- a/cms_theme/templates/timeline/milestone_card.html +++ b/cms_theme/templates/timeline/milestone_card.html @@ -1,28 +1,30 @@ {% load cms_tags frontend %} -
-
+
+
{% if instance.label %} -

{{ instance.label }}

+

{% inline_field instance "label" %}

{% endif %} {% if instance.heading %} -

{{ instance.heading }}

+

{% inline_field instance "heading" %}

{% endif %} -
+
{% for plugin in instance|get_slot:"image_top" %} {% render_plugin plugin %} {% endfor %} {% if instance.text %} -
- {{ instance.text|safe }} +
+ {% inline_field instance "text" "" "safe" %}
{% endif %} - {% for plugin in instance|get_slot:"image_top" %} + {% for plugin in instance|get_slot:"image_bottom" %} {% render_plugin plugin %} {% endfor %} - {% for plugin in instance|get_slot:"links" %} + {% for plugin in instance|get_slot:"links" %} + {% if forloop.first %}{% endif %} {% endfor %}
From bdeab408421e43f9ac6245c604a6262a43b891a2 Mon Sep 17 00:00:00 2001 From: maxnoelp2 Date: Mon, 23 Mar 2026 16:39:20 +0100 Subject: [PATCH 3/4] feat: Add eyebrow text, title, and text fields to TimelineContainer and update timeline template --- backend/static/scss/_timeline.scss | 6 ++++++ cms_theme/cms_components.py | 15 +++++++++++++++ cms_theme/templates/timeline/timeline.html | 10 +++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/static/scss/_timeline.scss b/backend/static/scss/_timeline.scss index 4b6e05d..e56b11a 100644 --- a/backend/static/scss/_timeline.scss +++ b/backend/static/scss/_timeline.scss @@ -134,4 +134,10 @@ $timeline-circle-size: 15px; p { margin-bottom: 0 !important; } +} + +.timeline-text { + p { + margin-bottom: 0 !important; + } } \ No newline at end of file diff --git a/cms_theme/cms_components.py b/cms_theme/cms_components.py index e80a20a..0c93ff0 100644 --- a/cms_theme/cms_components.py +++ b/cms_theme/cms_components.py @@ -153,6 +153,21 @@ class Meta: "Attributes", ] + eyebrow_text = forms.CharField( + label=_("Eyebrow text"), + required=False, + ) + + title = forms.CharField( + label=_("Title"), + required=False, + ) + + text = HTMLFormField( + label=_("Text"), + required=False, + ) + divider_color = forms.ChoiceField( label=_("Divider line color"), choices=frontend_settings.COLOR_STYLE_CHOICES, diff --git a/cms_theme/templates/timeline/timeline.html b/cms_theme/templates/timeline/timeline.html index d80195b..5748486 100644 --- a/cms_theme/templates/timeline/timeline.html +++ b/cms_theme/templates/timeline/timeline.html @@ -7,11 +7,11 @@ >
{# Header content: Heading and intro text #} - {% for child in instance.child_plugin_instances %} - {% if child.plugin_type != "MilestoneCardPlugin" %} - {% render_plugin child %} - {% endif %} - {% endfor %} +
+

{{ instance.eyebrow_text }}

+

{{ instance.title }}

+
{{ instance.text|safe }}
+
{# Timeline items #}
From e78c8604ee4c69660f1916be88117a1824ec143c Mon Sep 17 00:00:00 2001 From: maxnoelp2 Date: Mon, 23 Mar 2026 16:39:29 +0100 Subject: [PATCH 4/4] feat: Add migration for new frontend UI models including ClientCard, CodeBlock, CounterContainer, Heading, MilestoneCard, ResponsiveTable, and Spacing --- ...block_countercontainer_heading_and_more.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py diff --git a/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py b/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py new file mode 100644 index 0000000..a2c33cb --- /dev/null +++ b/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py @@ -0,0 +1,99 @@ +# Generated by Django 6.0.3 on 2026-03-23 15:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms_theme', '0006_benefitscard_benefitspanel_carouselitem_and_more'), + ('djangocms_frontend', '0002_migrate_links'), + ] + + operations = [ + migrations.CreateModel( + name='ClientCard', + fields=[ + ], + options={ + 'verbose_name': 'Client Card', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='CodeBlock', + fields=[ + ], + options={ + 'verbose_name': 'Code Block', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='CounterContainer', + fields=[ + ], + options={ + 'verbose_name': 'Counter Panel', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='Heading', + fields=[ + ], + options={ + 'verbose_name': 'Heading', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='MilestoneCard', + fields=[ + ], + options={ + 'verbose_name': 'Milestone Card', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='ResponsiveTable', + fields=[ + ], + options={ + 'verbose_name': 'Responsive Table', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.CreateModel( + name='Spacing', + fields=[ + ], + options={ + 'verbose_name': 'Spacing', + 'managed': False, + 'proxy': True, + }, + bases=('djangocms_frontend.frontenduiitem',), + ), + migrations.AlterModelOptions( + name='container1coltext', + options={'managed': False, 'verbose_name': 'Single Column'}, + ), + migrations.AlterModelOptions( + name='containerwithgrid', + options={'managed': False, 'verbose_name': 'Grid Section'}, + ), + ]