Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions backend/static/scss/_timeline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -115,3 +129,15 @@ $timeline-circle-size: 15px;
left: 0;
}
}

.milestone-text {
p {
margin-bottom: 0 !important;
}
}

.timeline-text {
p {
margin-bottom: 0 !important;
}
}
101 changes: 83 additions & 18 deletions cms_theme/cms_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,6 +18,8 @@
)
from djangocms_frontend.helpers import first_choice

from .fields import ColorChoiceField


# Common logger
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -197,7 +197,7 @@ class Meta:
render_template = "timeline/timeline.html"
allow_children = True
child_classes = [
"CardPlugin",
"MilestoneCardPlugin",
"TextPlugin",
"HeadingPlugin",
"SpacingPlugin",
Expand All @@ -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,
Expand All @@ -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"""
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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'},
),
]
32 changes: 32 additions & 0 deletions cms_theme/templates/timeline/milestone_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% load cms_tags frontend %}

<article id="milestone-{{ instance.id }}" class="milestone-card">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an <article> tag?

<div class="card shadow-sm {{ instance.get_classes }}">
<div class="card-body">
{% if instance.label %}
<h2 class="mb-2 pb-1">{% inline_field instance "label" %}</h2>
{% endif %}
{% if instance.heading %}
<h3 class="text-primary mb-4 pb-1">{% inline_field instance "heading" %}</h3>
{% endif %}
<div class="d-flex flex-column gap-4">
{% for plugin in instance|get_slot:"image_top" %}
{% render_plugin plugin %}
{% endfor %}
{% if instance.text %}
<div class="milestone-text">
{% inline_field instance "text" "" "safe" %}
</div>
{% endif %}
{% for plugin in instance|get_slot:"image_bottom" %}
{% render_plugin plugin %}
{% endfor %}
{% for plugin in instance|get_slot:"links" %}
{% if forloop.first %}<div class="milestone-links">{% endif %}
{% render_plugin plugin %}
{% if forloop.last %}</div>{% endif %}
{% endfor %}
</div>
</div>
</div>
</article>
12 changes: 6 additions & 6 deletions cms_theme/templates/timeline/timeline.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
>
<div class="container">
{# Header content: Heading and intro text #}
{% for child in instance.child_plugin_instances %}
{% if child.plugin_type != "CardPlugin" %}
{% render_plugin child %}
{% endif %}
{% endfor %}
<div class="text-center pb-7">
<p class="overline text-dark">{{ instance.eyebrow_text }}</p>
<h2 class="pt-2 pb-4 mb-0 text-secondary">{{ instance.title }}</h2>
<div class="timeline-text text-dark">{{ instance.text|safe }}</div>
</div>

{# Timeline items #}
<div class="main-timeline timeline-divider-{{ instance.config.divider_color|default:'primary' }} timeline-circle-{{ instance.config.circle_color|default:'secondary' }}" role="list">
{% 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 %}
<div class="timeline {% cycle 'timeline-right' 'timeline-left' %}" role="listitem">
{% render_plugin child %}
</div>
Expand Down