diff --git a/backend/static/scss/_timeline.scss b/backend/static/scss/_timeline.scss index 49f613f..e56b11a 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,15 @@ $timeline-circle-size: 15px; left: 0; } } + +.milestone-text { + 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 63ba8d6..df1de46 100644 --- a/cms_theme/cms_components.py +++ b/cms_theme/cms_components.py @@ -10,8 +10,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, @@ -20,6 +18,8 @@ ) from djangocms_frontend.helpers import first_choice +from .fields import ColorChoiceField + # Common logger logger = logging.getLogger(__name__) @@ -197,7 +197,7 @@ class Meta: render_template = "timeline/timeline.html" allow_children = True child_classes = [ - "CardPlugin", + "MilestoneCardPlugin", "TextPlugin", "HeadingPlugin", "SpacingPlugin", @@ -211,6 +211,21 @@ class Meta: "padding_y": "py-6", } + 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, @@ -230,6 +245,44 @@ 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"]), + ) + frontend_editable_fields = ("label", "heading", "text") + + 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""" @@ -1066,6 +1119,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" @@ -1198,12 +1252,16 @@ def _fetch_github_stat(self, counter_type): data = resp.json() return data.get("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, ) @@ -1213,7 +1271,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, ) @@ -1253,16 +1313,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( @@ -1301,7 +1364,9 @@ 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") + ) @components.register @@ -1342,7 +1407,7 @@ class Meta: def get_short_description(self) -> str: heading = self.config.get("heading") - background_context = self.config.get('background_context', 'none') + background_context = self.config.get("background_context", "none") if heading: return f"{heading} ({background_context})" return background_context 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'}, + ), + ] diff --git a/cms_theme/templates/timeline/milestone_card.html b/cms_theme/templates/timeline/milestone_card.html new file mode 100644 index 0000000..0e0ffc2 --- /dev/null +++ b/cms_theme/templates/timeline/milestone_card.html @@ -0,0 +1,32 @@ +{% load cms_tags frontend %} + +
+
+
+ {% if instance.label %} +

{% inline_field instance "label" %}

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

{% inline_field instance "heading" %}

+ {% endif %} +
+ {% for plugin in instance|get_slot:"image_top" %} + {% render_plugin plugin %} + {% endfor %} + {% if instance.text %} +
+ {% inline_field instance "text" "" "safe" %} +
+ {% endif %} + {% for plugin in instance|get_slot:"image_bottom" %} + {% render_plugin plugin %} + {% endfor %} + {% for plugin in instance|get_slot:"links" %} + {% if forloop.first %}{% endif %} + {% 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..5748486 100644 --- a/cms_theme/templates/timeline/timeline.html +++ b/cms_theme/templates/timeline/timeline.html @@ -7,16 +7,16 @@ >
{# Header content: Heading and intro text #} - {% for child in instance.child_plugin_instances %} - {% if child.plugin_type != "CardPlugin" %} - {% render_plugin child %} - {% endif %} - {% endfor %} +
+

{{ instance.eyebrow_text }}

+

{{ instance.title }}

+
{{ instance.text|safe }}
+
{# 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 %}