Refactor event calendar HTML and integrate Bootstrap CSS

Revised the event calendar template for improved structure, semantics, and styling consistency. Added Bootstrap CSS for enhanced styling possibilities and updated the ICS link with a dedicated Django URL tag.
main
Arne Schauf 2 weeks ago
parent f79628ac31
commit 24674b70a3
  1. 1
      .gitignore
  2. 18
      core/migrations/0013_blogpage_subtitle.py
  3. 17
      core/migrations/0014_eventpage_is_external.py
  4. 29
      core/migrations/0015_remove_eventpage_external_registration_code_and_more.py
  5. 197
      core/models.py
  6. 1030
      core/static/css/theme.css
  7. 96
      core/static/css/timeline.css
  8. BIN
      core/static/fonts/roboto-v30-latin-100.woff
  9. BIN
      core/static/fonts/roboto-v30-latin-100.woff2
  10. BIN
      core/static/fonts/roboto-v30-latin-200.woff
  11. BIN
      core/static/fonts/roboto-v30-latin-200.woff2
  12. BIN
      core/static/fonts/roboto-v30-latin-300.woff
  13. BIN
      core/static/fonts/roboto-v30-latin-300.woff2
  14. BIN
      core/static/fonts/roboto-v30-latin-500.woff
  15. BIN
      core/static/fonts/roboto-v30-latin-500.woff2
  16. BIN
      core/static/fonts/roboto-v30-latin-700.woff
  17. BIN
      core/static/fonts/roboto-v30-latin-700.woff2
  18. BIN
      core/static/fonts/roboto-v30-latin-regular.woff
  19. BIN
      core/static/fonts/roboto-v30-latin-regular.woff2
  20. 48
      core/static/fonts/roboto.css
  21. BIN
      core/static/img/250430__Logo black-mit Schriftzug.png
  22. BIN
      core/static/img/logo_feo.png
  23. BIN
      core/static/img/logo_feo_old.png
  24. BIN
      core/static/img/logo_feo_white.png
  25. BIN
      core/static/img/logo_feo_white_old.png
  26. 2018
      core/static/vendor/bootstrap-icons/bootstrap-icons.css
  27. BIN
      core/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff
  28. BIN
      core/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2
  29. 7
      core/static/vendor/bootstrap/bootstrap.bundle.min.js
  30. 6
      core/static/vendor/bootstrap/bootstrap.min.css
  31. 53
      core/tasks.py
  32. 67
      core/templates/bootstrap5_base.html
  33. 366
      core/templates/core/blog_page.html
  34. 123
      core/templates/core/event_history_page.html
  35. 99
      core/templates/core/event_index_page.html
  36. 597
      core/templates/core/event_page.html
  37. 475
      core/templates/core/home_page.html
  38. 17
      core/templates/core/tags/bootstrap5_top_menu.html
  39. 7
      core/templates/core/tags/bootstrap5_top_menu_children.html
  40. 57
      core/templates/core/tags/event_calendar.html
  41. 8
      core/templates/core/tags/newsletter_minimal_form.html
  42. 16
      core/templatetags/core_tags.py
  43. 25
      docker-compose.yml
  44. 5
      feo_homepage/__init__.py
  45. 14
      feo_homepage/celery.py
  46. 15
      feo_homepage/settings/base.py
  47. 2
      requirements.in
  48. 39
      requirements.txt

1
.gitignore vendored

@ -1,3 +1,4 @@
celerybeat-schedule*
*.ipynb *.ipynb
docker-compose.override.yml docker-compose.override.yml
.env .env

@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-04-30 06:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_aboutuspage'),
]
operations = [
migrations.AddField(
model_name='blogpage',
name='subtitle',
field=models.CharField(blank=True, default='Aktuelle Neuigkeiten, Einblicke und Ankündigungen', max_length=512),
),
]

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_blogpage_subtitle'),
]
operations = [
migrations.AddField(
model_name='eventpage',
name='is_external',
field=models.BooleanField(default=False, help_text='Externes Event? Externe Events werden nicht im Upcoming Events Banner auf der Startseite angezeigt.'),
),
]

@ -0,0 +1,29 @@
# Generated by Django 5.2 on 2025-04-30 10:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0014_eventpage_is_external'),
]
operations = [
migrations.RemoveField(
model_name='eventpage',
name='external_registration_code',
),
migrations.RemoveField(
model_name='eventpage',
name='from_address',
),
migrations.RemoveField(
model_name='eventpage',
name='subject',
),
migrations.RemoveField(
model_name='eventpage',
name='to_address',
),
]

@ -156,26 +156,26 @@ class HomePage(Page):
REVOLUTION_SLIDER_FIELDS = [ REVOLUTION_SLIDER_FIELDS = [
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('slide1_img'), FieldPanel('slide1_img'),
FieldPanel('slide1_headline', classname="full"), FieldPanel('slide1_headline'),
FieldPanel('slide1_subline', classname="full"), FieldPanel('slide1_subline'),
FieldPanel('slide1_link_url', classname="full"), FieldPanel('slide1_link_url'),
FieldPanel('slide1_link_text', classname="full"), FieldPanel('slide1_link_text'),
], heading='Slide 1', classname="collapsible"), ], heading='Slide 1', classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('slide2_img'), FieldPanel('slide2_img'),
FieldPanel('slide2_headline', classname="full"), FieldPanel('slide2_headline'),
FieldPanel('slide2_subline', classname="full"), FieldPanel('slide2_subline'),
FieldPanel('slide2_link_url', classname="full"), FieldPanel('slide2_link_url'),
FieldPanel('slide2_link_text', classname="full"), FieldPanel('slide2_link_text'),
], heading='Slide 2', classname="collapsible"), ], heading='Slide 2', classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('slide3_img'), FieldPanel('slide3_img'),
FieldPanel('slide3_headline', classname="full"), FieldPanel('slide3_headline'),
FieldPanel('slide3_subline', classname="full"), FieldPanel('slide3_subline'),
FieldPanel('slide3_link_url', classname="full"), FieldPanel('slide3_link_url'),
FieldPanel('slide3_link_text', classname="full"), FieldPanel('slide3_link_text'),
], heading='Slide 3'), ], heading='Slide 3'),
] ]
@ -183,30 +183,30 @@ HOME_THUMBNAIL_FIELDS = [
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('thumbnail1_img'), FieldPanel('thumbnail1_img'),
PageChooserPanel('thumbnail1_more_page'), PageChooserPanel('thumbnail1_more_page'),
FieldPanel('thumbnail1_more_text', classname="full"), FieldPanel('thumbnail1_more_text'),
FieldPanel('thumbnail1_title', classname="full"), FieldPanel('thumbnail1_title'),
FieldPanel('thumbnail1_text', classname="full"), FieldPanel('thumbnail1_text'),
], heading='Thumbnail 1', classname="collapsible"), ], heading='Thumbnail 1', classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('thumbnail2_img'), FieldPanel('thumbnail2_img'),
PageChooserPanel('thumbnail2_more_page'), PageChooserPanel('thumbnail2_more_page'),
FieldPanel('thumbnail2_more_text', classname="full"), FieldPanel('thumbnail2_more_text'),
FieldPanel('thumbnail2_title', classname="full"), FieldPanel('thumbnail2_title'),
FieldPanel('thumbnail2_text', classname="full"), FieldPanel('thumbnail2_text'),
], heading='Thumbnail 2', classname="collapsible"), ], heading='Thumbnail 2', classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('thumbnail3_img'), FieldPanel('thumbnail3_img'),
PageChooserPanel('thumbnail3_more_page'), PageChooserPanel('thumbnail3_more_page'),
FieldPanel('thumbnail3_more_text', classname="full"), FieldPanel('thumbnail3_more_text'),
FieldPanel('thumbnail3_title', classname="full"), FieldPanel('thumbnail3_title'),
FieldPanel('thumbnail3_text', classname="full"), FieldPanel('thumbnail3_text'),
], heading='Thumbnail 3', classname="collapsible"), ], heading='Thumbnail 3', classname="collapsible"),
] ]
HomePage.content_panels = [ HomePage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"), MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"),
FieldPanel('block1', classname="full"), FieldPanel('block1'),
MultiFieldPanel(HOME_THUMBNAIL_FIELDS, heading='Thumbnail elements', classname="collapsible collapsed"), MultiFieldPanel(HOME_THUMBNAIL_FIELDS, heading='Thumbnail elements', classname="collapsible collapsed"),
] ]
@ -225,11 +225,16 @@ class SimpleOneColumnPage(Page):
SimpleOneColumnPage.content_panels = [ SimpleOneColumnPage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('featured_media'), FieldPanel('featured_media'),
FieldPanel('content', classname="full"), FieldPanel('content'),
] ]
class BlogPage(Page): class BlogPage(Page):
subtitle = models.CharField(
max_length=512,
default="Aktuelle Neuigkeiten, Einblicke und Ankündigungen",
blank=True
)
posts = StreamField([ posts = StreamField([
('post', blocks.StructBlock([ ('post', blocks.StructBlock([
('images', blocks.ListBlock(ImageChooserBlock())), ('images', blocks.ListBlock(ImageChooserBlock())),
@ -244,6 +249,7 @@ class BlogPage(Page):
BlogPage.content_panels = [ BlogPage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('subtitle'),
FieldPanel('posts'), FieldPanel('posts'),
] ]
@ -258,7 +264,7 @@ class EventIndexPage(Page):
description = "Displays all events on EventPages below in an overview. Use this as parent page for events." description = "Displays all events on EventPages below in an overview. Use this as parent page for events."
class EventPage(AbstractEmailForm): class EventPage(Page):
parent_page_types = ['core.EventIndexPage', 'core.EventHistoryPage'] parent_page_types = ['core.EventIndexPage', 'core.EventHistoryPage']
subtitle = models.CharField(max_length=512, null=True, blank=True) subtitle = models.CharField(max_length=512, null=True, blank=True)
description = RichTextField(blank=True) description = RichTextField(blank=True)
@ -358,6 +364,22 @@ class EventPage(AbstractEmailForm):
('muted', blocks.BooleanBlock(label='Stummgeschaltet starten', required=False)), ('muted', blocks.BooleanBlock(label='Stummgeschaltet starten', required=False)),
('post_content', blocks.RichTextBlock(required=False)), ('post_content', blocks.RichTextBlock(required=False)),
])), ])),
('image_tab', blocks.StructBlock([
('title', blocks.CharBlock(max_length=64)),
('image', ImageChooserBlock()),
('caption', blocks.CharBlock(max_length=256, required=False)),
('size', blocks.ChoiceBlock(choices=[
('small', 'Small (25% width)'),
('medium', 'Medium (50% width)'),
('large', 'Large (75% width)'),
('full', 'Full width (100%)'),
], default='large')),
('alignment', blocks.ChoiceBlock(choices=[
('left', 'Left'),
('center', 'Center'),
('right', 'Right'),
], default='center')),
])),
], null=True, blank=True, use_json_field=True) ], null=True, blank=True, use_json_field=True)
registration_url = models.URLField(max_length=512, blank=True, help_text="URL der Anmeldeseite (leer lassen um klassische Anmeldung zu verwenden)") registration_url = models.URLField(max_length=512, blank=True, help_text="URL der Anmeldeseite (leer lassen um klassische Anmeldung zu verwenden)")
@ -369,11 +391,9 @@ class EventPage(AbstractEmailForm):
help_text="Last day where the registration will be available. Leave empty to enable registration permanently" help_text="Last day where the registration will be available. Leave empty to enable registration permanently"
"after start date") "after start date")
show_in_event_calendar = models.BooleanField(default=True, help_text='Event in Eventliste und im Kalender anzeigen?') show_in_event_calendar = models.BooleanField(default=True, help_text='Event in Eventliste und im Kalender anzeigen?')
is_external = models.BooleanField(default=False, help_text='Externes Event? Externe Events werden nicht im Upcoming Events Banner auf der Startseite angezeigt.')
is_free = models.BooleanField(default=False, help_text='If this is checked the send button under the registration' is_free = models.BooleanField(default=False, help_text='If this is checked the send button under the registration'
'form will be labeled "verbindlich anmelden" instead of "kostenpflichtig anmelden"') 'form will be labeled "verbindlich anmelden" instead of "kostenpflichtig anmelden"')
external_registration_code = models.TextField(
blank=True, null=True,
help_text='Put code e.g. from Xing here. If this field is filled out there will be no registration form displayed.')
vimeo_id = models.TextField(blank=True, help_text='Vimeo Event ID (aus Vimeo-Link: https://vimeo.com/<hier-ist-die-id> )') vimeo_id = models.TextField(blank=True, help_text='Vimeo Event ID (aus Vimeo-Link: https://vimeo.com/<hier-ist-die-id> )')
@ -406,38 +426,32 @@ class EventRegistrationField(AbstractFormField):
EventPage.content_panels = [ EventPage.content_panels = [
MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"), MultiFieldPanel(REVOLUTION_SLIDER_FIELDS, heading='Slider elements', classname="collapsible collapsed"),
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('subtitle', classname="full"), FieldPanel('subtitle'),
FieldPanel('description', classname="full"), FieldPanel('description'),
FieldPanel('img1_img'), FieldPanel('img1_img'),
FieldPanel('img1_caption', classname="full"), FieldPanel('img1_caption'),
FieldPanel('img2_img'), FieldPanel('img2_img'),
FieldPanel('img2_caption', classname="full"), FieldPanel('img2_caption'),
FieldPanel('start_date', classname="full"), FieldPanel('start_date'),
FieldPanel('end_date', classname="full"), FieldPanel('end_date'),
FieldPanel('tabs'), FieldPanel('tabs'),
FieldPanel('location_name', classname="full"), FieldPanel('location_name'),
FieldPanel('location_street', classname="full"), FieldPanel('location_street'),
FieldPanel('location_city', classname="full"), FieldPanel('location_city'),
FieldPanel('flyer'), FieldPanel('flyer'),
FieldPanel('downloads'), FieldPanel('downloads'),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('show_in_event_calendar', classname="full"), FieldPanel('show_in_event_calendar'),
FieldPanel('is_free', classname="full"), FieldPanel('is_free'),
FieldPanel('pretix_slug', classname="full"), FieldPanel('is_external'),
FieldPanel('registration_url', classname="full"), FieldPanel('pretix_slug'),
FieldPanel('vimeo_id', classname="full"), FieldPanel('registration_url'),
FieldPanel('vimeo_id'),
], heading="Settings", classname="collapsible collapsed"), ], heading="Settings", classname="collapsible collapsed"),
FieldPanel('external_registration_code', classname="full"),
MultiFieldPanel([ MultiFieldPanel([
InlinePanel('form_fields', label="Form fields"), FieldPanel('registration_start_date'),
FieldPanel('registration_start_date', classname="full"), FieldPanel('registration_end_date'),
FieldPanel('registration_end_date', classname="full"), ], heading="Registration Settings", classname="collapsible collapsed"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email"),
], heading="Registration Form", classname="collapsible collapsed"),
] ]
@ -458,6 +472,45 @@ class EventHistoryPage(Page):
])), ])),
], null=True, blank=True, use_json_field=True) ], null=True, blank=True, use_json_field=True)
def get_all_events(self):
"""
Returns a list of all events, including both manual entries from the StreamField
and child EventPage objects, sorted by start_date.
"""
# Get manual entries from StreamField
manual_events = []
for event_block in self.events:
manual_events.append({
'type': 'manual',
'value': event_block.value
})
# Get child EventPage objects
from core.models import EventPage
child_events = []
for child in self.get_children().specific():
if isinstance(child, EventPage):
child_events.append({
'type': 'page',
'value': {
'title': child.title,
'subtitle': child.subtitle,
'start_date': child.start_date,
'end_date': child.end_date,
'location_name': child.location_name,
'location_city': child.location_city,
'image': child.img1_img,
'flyer': child.flyer,
'additional_info_btn_label': '',
'additional_info_target': None,
},
'page': child
})
# Combine and sort all events by start_date
all_events = manual_events + child_events
return sorted(all_events, key=lambda x: x['value']['start_date'], reverse=True)
EventHistoryPage.content_panels = [ EventHistoryPage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
@ -489,20 +542,20 @@ class ContactFormPage(WagtailCaptchaEmailForm):
ContactFormPage.content_panels = [ ContactFormPage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('form_title', classname="full"), FieldPanel('form_title'),
FieldPanel('person_img'), FieldPanel('person_img'),
FieldPanel('person_name', classname="full"), FieldPanel('person_name'),
FieldPanel('person_position', classname="full"), FieldPanel('person_position'),
FieldPanel('address', classname="full"), FieldPanel('address'),
FieldPanel('mail_address', classname="full"), FieldPanel('mail_address'),
FieldPanel('phone', classname="full"), FieldPanel('phone'),
FieldPanel('web_address', classname="full"), FieldPanel('web_address'),
InlinePanel('form_fields', label="Form fields"), InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"), FieldPanel('thank_you_text'),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('to_address', classname="full"), FieldPanel('to_address'),
FieldPanel('from_address', classname="full"), FieldPanel('from_address'),
FieldPanel('subject', classname="full"), FieldPanel('subject'),
], "Email") ], "Email")
] ]
@ -572,21 +625,21 @@ class AboutUsPage(Page):
AboutUsPage.content_panels = [ AboutUsPage.content_panels = [
FieldPanel('title', classname="full title"), FieldPanel('title', classname="full title"),
FieldPanel('company_intro', classname="full"), FieldPanel('company_intro'),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('mission_title', classname="full"), FieldPanel('mission_title'),
FieldPanel('mission_text', classname="full"), FieldPanel('mission_text'),
FieldPanel('vision_title', classname="full"), FieldPanel('vision_title'),
FieldPanel('vision_text', classname="full"), FieldPanel('vision_text'),
], heading="Mission and Vision", classname="collapsible"), ], heading="Mission and Vision", classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('history_title', classname="full"), FieldPanel('history_title'),
FieldPanel('history_text', classname="full"), FieldPanel('history_text'),
], heading="Company History", classname="collapsible"), ], heading="Company History", classname="collapsible"),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('team_title', classname="full"), FieldPanel('team_title'),
FieldPanel('team_intro', classname="full"), FieldPanel('team_intro'),
FieldPanel('team_members'), FieldPanel('team_members'),
], heading="Team Members", classname="collapsible"), ], heading="Team Members", classname="collapsible"),
FieldPanel('additional_content', classname="full"), FieldPanel('additional_content'),
] ]

File diff suppressed because it is too large Load Diff

@ -0,0 +1,96 @@
/* Timeline styling */
.timeline {
position: relative;
padding: 0;
list-style: none;
}
.timeline:before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 40px;
width: 4px;
background-color: var(--bs-primary);
}
.timeline-item {
position: relative;
margin-bottom: 50px;
}
.timeline-badge {
position: absolute;
top: 0;
left: 40px;
width: 60px;
height: 60px;
margin-left: -30px;
border-radius: 50%;
text-align: center;
background-color: var(--bs-primary);
color: white;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.timeline-badge-day {
font-size: 1.25rem;
font-weight: bold;
line-height: 1;
}
.timeline-badge-month {
font-size: 0.8rem;
text-transform: uppercase;
line-height: 1;
}
.timeline-month-header {
position: relative;
margin-bottom: 30px;
list-style: none;
}
.timeline-month-header h3 {
position: relative;
padding-left: 60px;
border-bottom: 2px solid var(--bs-primary);
padding-bottom: 10px;
}
.timeline-panel {
position: relative;
width: calc(100% - 90px);
float: right;
border-radius: 8px;
}
.timeline-panel:before {
content: '';
position: absolute;
top: 26px;
left: -15px;
border-top: 15px solid transparent;
border-right: 15px solid #fff;
border-bottom: 15px solid transparent;
}
.timeline-date {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: var(--bs-primary);
}
@media (max-width: 767px) {
.timeline-badge {
margin-left: 0;
}
}

@ -0,0 +1,48 @@
/* Roboto Font */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url('./roboto-v30-latin-100.woff2') format('woff2'),
url('./roboto-v30-latin-100.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 200;
src: url('./roboto-v30-latin-200.woff2') format('woff2'),
url('./roboto-v30-latin-200.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('./roboto-v30-latin-300.woff2') format('woff2'),
url('./roboto-v30-latin-300.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('./roboto-v30-latin-regular.woff2') format('woff2'),
url('./roboto-v30-latin-regular.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('./roboto-v30-latin-500.woff2') format('woff2'),
url('./roboto-v30-latin-500.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url('./roboto-v30-latin-700.woff2') format('woff2'),
url('./roboto-v30-latin-700.woff') format('woff');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,53 @@
from datetime import date
from celery import shared_task
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from core.models import EventPage, EventHistoryPage
@shared_task
def move_past_events_to_history():
"""
Daily task to:
1. Move past events that are not external to the history
2. Unpublish external events that are past
"""
today = date.today()
# Find all live EventPage objects with a start_date in the past
past_events = EventPage.objects.live().filter(
Q(end_date__lt=timezone.now()) | Q(end_date__isnull=True, start_date__lt=timezone.now()))
# Find the first EventHistoryPage to use as the target for moving events
try:
history_page = EventHistoryPage.objects.live().first()
if not history_page:
# If no history page exists, we can't move events
return "No EventHistoryPage found. Cannot move past events."
except EventHistoryPage.DoesNotExist:
return "No EventHistoryPage found. Cannot move past events."
moved_count = 0
unpublished_count = 0
with transaction.atomic():
# Process each past event
for event in past_events:
if event.is_external:
# Unpublish external events
event.unpublish()
unpublished_count += 1
else:
# Move non-external events to history
# Get the current parent page
current_parent = event.get_parent()
# Only move if not already under the history page
if current_parent.id != history_page.id:
# Move the event to be a child of the history page
event.move(history_page, pos='last-child')
moved_count += 1
return f"Processed past events: {moved_count} moved to history, {unpublished_count} external events unpublished."

@ -11,16 +11,14 @@
<!-- Favicon --> <!-- Favicon -->
<link rel="shortcut icon" href="{% static 'favicon.png' %}"> <link rel="shortcut icon" href="{% static 'favicon.png' %}">
<!-- Google Fonts - Roboto --> <!-- Google Fonts - Roboto (Local) -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="{% static 'fonts/roboto.css' %}">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Bootstrap 5 CSS --> <!-- Bootstrap 5 CSS (Local) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="{% static 'vendor/bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- Bootstrap Icons --> <!-- Bootstrap Icons (Local) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> <link rel="stylesheet" href="{% static 'vendor/bootstrap-icons/bootstrap-icons.css' %}">
<!-- Custom Theme CSS --> <!-- Custom Theme CSS -->
<link rel="stylesheet" href="{% static 'css/theme.css' %}"> <link rel="stylesheet" href="{% static 'css/theme.css' %}">
@ -35,19 +33,6 @@
<!-- Header --> <!-- Header -->
<header> <header>
<!-- Top Bar -->
<div class="bg-light py-2">
<div class="container">
<div class="d-flex justify-content-end">
<ul class="nav">
<li class="nav-item"><a class="nav-link" href="{% slugurl 'contact' %}">Kontakt</a></li>
<li class="nav-item"><a class="nav-link" href="{% slugurl 'imprint' %}">Impressum</a></li>
<li class="nav-item"><a class="nav-link" href="{% slugurl 'datenschutz' %}">Datenschutz</a></li>
</ul>
</div>
</div>
</div>
<!-- Navigation --> <!-- Navigation -->
{% bootstrap5_top_menu parent=site_root calling_page=self %} {% bootstrap5_top_menu parent=site_root calling_page=self %}
@ -94,10 +79,9 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-4 mb-4 mb-md-0"> <div class="col-md-4 mb-4 mb-md-0">
<h2 class="h4 mb-3 text-primary">FEO GmbH</h2> <h2 class="h4 mb-3">FEO GmbH</h2>
<p class="mb-4">{% footer_about %}</p> <p class="mb-4">{% footer_about %}</p>
<h2 class="h4 mb-3 text-primary">Newsletter</h2>
{% footer_newsletter %} {% footer_newsletter %}
</div> </div>
@ -106,7 +90,7 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h2 class="h4 mb-3 text-primary">Kontakt</h2> <h2 class="h4 mb-3">Kontakt</h2>
<address> <address>
<p> <p>
FEO Gesellschaft für Fortbildungs-<br> FEO Gesellschaft für Fortbildungs-<br>
@ -119,13 +103,16 @@
</p> </p>
</address> </address>
<h2 class="h4 mb-3 text-primary">Stay Connected</h2> <h2 class="h4 mb-3">Stay Connected</h2>
<div class="d-flex gap-2"> <div class="d-flex flex-column gap-2">
<a href="https://www.linkedin.com/in/marion-s-a2b81152/" target="_blank" class="btn btn-outline-primary"> <a href="https://www.linkedin.com/in/marion-schauf-a2b81152/" target="_blank" class="btn btn-outline-primary social-btn">
<i class="bi bi-linkedin"></i> <i class="bi bi-linkedin me-2"></i>LinkedIn
</a>
<a href="https://www.xing.com/companies/feogesellschaftf%C3%BCrfortbildungs-undeventorganisationmbh" target="_blank" class="btn btn-outline-primary social-btn">
<i class="bi bi-x me-2"></i>Xing
</a> </a>
<a href="https://www.xing.com/companies/feogesellschaftf%C3%BCrfortbildungs-undeventorganisationmbh" target="_blank" class="btn btn-outline-primary"> <a href="https://www.instagram.com/feogmbh/" target="_blank" class="btn btn-outline-primary social-btn">
<i class="bi bi-x"></i> <i class="bi bi-instagram me-2"></i>Instagram
</a> </a>
</div> </div>
</div> </div>
@ -152,8 +139,24 @@
</div> </div>
</footer> </footer>
<!-- Bootstrap 5 JS Bundle with Popper --> <!-- Bootstrap 5 JS Bundle with Popper (Local) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="{% static 'vendor/bootstrap/bootstrap.bundle.min.js' %}"></script>
<!-- Navbar scroll effect -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const navbar = document.querySelector('.modern-navbar');
if (navbar) {
window.addEventListener('scroll', function() {
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
}
});
</script>
{% block extra_js %} {% block extra_js %}
{% endblock %} {% endblock %}

@ -1,56 +1,296 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags crispy_forms_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags crispy_forms_tags %}
{% block extra_css %}
<style>
/* Blog page specific styles */
.blog-post {
margin-bottom: 3rem;
transition: transform 0.3s ease;
}
.blog-post:hover {
transform: translateY(-5px);
}
.blog-post-image {
position: relative;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
}
.blog-post-image img {
transition: transform 0.5s ease;
width: 100%;
height: 100%;
height: 450px;
object-fit: contain;
}
.blog-post:hover .blog-post-image img {
transform: scale(1.05);
}
.blog-post-content {
padding: 1.5rem;
background: #fff;
border-radius: 12px;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
height: 100%;
}
.blog-post-meta {
display: flex;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.blog-post-meta-item {
display: flex;
align-items: center;
margin-right: 1.5rem;
color: #6c757d;
font-size: 0.9rem;
}
.blog-post-meta-item i {
margin-right: 0.5rem;
color: var(--bs-primary);
}
.blog-post-title {
font-size: 1.75rem;
margin-bottom: 1rem;
font-weight: 700;
color: #333;
transition: color 0.3s ease;
}
.blog-post:hover .blog-post-title {
color: var(--bs-primary);
}
.blog-post-excerpt {
color: #6c757d;
margin-bottom: 1.5rem;
}
.blog-post-link {
display: inline-flex;
align-items: center;
font-weight: 600;
color: var(--bs-primary);
transition: all 0.3s ease;
}
.blog-post-link i {
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.blog-post-link:hover {
color: #65ab27;
text-decoration: none;
}
.blog-post-link:hover i {
transform: translateX(5px);
}
.carousel-blog {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
height: 100%;
}
.carousel-blog .carousel-inner {
height: 100%;
}
.carousel-blog .carousel-item {
height: 100%;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
}
.carousel-blog .carousel-item img {
object-fit: contain;
height: 450px;
}
.carousel-blog .carousel-control-prev,
.carousel-blog .carousel-control-next {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.carousel-blog:hover .carousel-control-prev,
.carousel-blog:hover .carousel-control-next {
opacity: 1;
}
.carousel-blog .carousel-control-prev {
left: 20px;
}
.carousel-blog .carousel-control-next {
right: 20px;
}
.carousel-blog .carousel-control-prev-icon,
.carousel-blog .carousel-control-next-icon {
filter: invert(1);
}
.carousel-blog .carousel-indicators {
bottom: 20px;
}
.carousel-blog .carousel-indicators button {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
margin: 0 5px;
}
.carousel-blog .carousel-indicators button.active {
background-color: var(--bs-primary);
}
.reading-time {
background-color: var(--bs-primary);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
display: inline-block;
}
@media (max-width: 768px) {
.blog-post-content {
margin-top: 1rem;
height: auto;
}
.blog-post-image,
.carousel-blog,
.carousel-blog .carousel-inner,
.carousel-blog .carousel-item {
height: auto;
min-height: 250px;
}
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container py-5">
{% for post in self.posts %} <div class="row mb-5">
<div class="row mb-4"> <div class="col-12 text-center">
{% if post.value.images|length == 1 %} <h1 class="display-4 fw-bold mb-3">{{ page.title }}</h1>
<div class="col-sm-5 mb-4 mb-sm-0"> <p class="lead text-muted">{{ page.subtitle }}</p>
{% image post.value.images.0 max-650x400 as img %} <div class="divider mx-auto my-4" style="width: 80px; height: 4px; background-color: var(--bs-primary);"></div>
{% image post.value.images.0 original as img_orig %} </div>
<a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#imageModal{{ img.id }}"> </div>
<img src="{{ img.url }}" width="{{ img.width }}"
height="{{ img.height }}" alt="{{ img.alt }}" class="img-fluid rounded" /> <div class="row">
</a> {% for post in self.posts %}
<div class="col-lg-12 blog-post">
<!-- Modal for single image --> <div class="row g-0 align-items-stretch">
<div class="modal fade" id="imageModal{{ img.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ img.id }}" aria-hidden="true"> <div class="col-md-5 d-flex">
<div class="modal-dialog modal-lg"> {% if post.value.images|length == 1 %}
<div class="modal-content"> <div class="blog-post-image">
<div class="modal-header"> {% image post.value.images.0 max-650x450 as img %}
<h5 class="modal-title" id="imageModalLabel{{ img.id }}">{{ img.alt }}</h5> {% image post.value.images.0 original as img_orig %}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#imgModal{{ img.id }}">
<img src="{{ img.url }}" alt="{{ img.alt }}" class="img-fluid" />
</a>
</div>
{% else %}
<div id="carousel-{{ post.id }}" class="carousel slide carousel-blog" data-bs-ride="carousel">
<div class="carousel-indicators">
{% for image in post.value.images %}
<button type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ forloop.counter }}"></button>
{% endfor %}
</div>
<div class="carousel-inner">
{% for image in post.value.images %}
{% image image max-650x450 as img %}
{% image image original as img_orig %}
<div class="carousel-item{% if forloop.first %} active{% endif %}">
<a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#imgModal{{ img.id }}">
<img src="{{ img.url }}" class="d-block w-100" alt="{{ img.alt }}" />
</a>
</div>
{% endfor %}
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
{% endif %}
</div> </div>
<div class="modal-body text-center"> <div class="col-md-7 d-flex">
<img src="{{ img_orig.url }}" alt="{{ img.alt }}" class="img-fluid" /> <div class="blog-post-content w-100">
<div class="blog-post-meta">
{% if post.value.author %}
<div class="blog-post-meta-item">
<i class="bi bi-person-circle"></i>
<span>{{ post.value.author }}</span>
</div>
{% endif %}
<div class="blog-post-meta-item">
<i class="bi bi-calendar-event"></i>
<span>{{ post.value.date }}</span>
</div>
<div class="blog-post-meta-item">
<i class="bi bi-clock"></i>
<span class="reading-time">{{ post.value.text|striptags|wordcount|divisibleby:200|add:1 }} min Lesezeit</span>
</div>
</div>
<h2 class="blog-post-title">{{ post.value.title }}</h2>
<div class="blog-post-excerpt">
{{ post.value.text|richtext }}
</div>
{% if post.value.url %}
<a href="{{ post.value.url }}" class="blog-post-link">
Mehr Infos <i class="bi bi-arrow-right"></i>
</a>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {% for image in post.value.images %}
</div> {% image image max-650x450 as img %}
{% else %}
<div class="col-sm-5 mb-4 mb-sm-0">
<div id="carousel-{{ post.id }}" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-indicators">
{% for image in post.value.images %}
<button type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ forloop.counter }}"></button>
{% endfor %}
</div>
<div class="carousel-inner">
{% for image in post.value.images %}
{% image image fill-650x400 as img %}
{% image image original as img_orig %} {% image image original as img_orig %}
<div class="carousel-item{% if forloop.first %} active{% endif %}"> <div class="modal fade" id="imgModal{{ img.id }}" tabindex="-1" aria-labelledby="carouselModalLabel{{ img.id }}" aria-hidden="true">
<a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#carouselModal{{ img.id }}"> <div class="modal-dialog modal-xl">
<img src="{{ img.url }}" class="d-block w-100" alt="{{ img.alt }}" />
</a>
</div>
<!-- Modal for carousel image -->
<div class="modal fade" id="carouselModal{{ img.id }}" tabindex="-1" aria-labelledby="carouselModalLabel{{ img.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="carouselModalLabel{{ img.id }}">{{ img.alt }}</h5> <h5 class="modal-title" id="carouselModalLabel{{ img.id }}">{{ img.alt }}</h5>
@ -62,42 +302,8 @@
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> {% endfor %}
<button class="carousel-control-prev" type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carousel-{{ post.id }}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>
{% endif %}
<div class="col-sm-7">
<div class="card border-0">
<div class="card-body p-0">
<ul class="list-inline text-muted small mb-3">
{% if post.value.author %}
<li class="list-inline-item">Von {{ post.value.author }}</li>
{% endif %}
<li class="list-inline-item">{{ post.value.date }}</li>
</ul>
<h2 class="mb-3">{{ post.value.title }}</h2>
<div class="mb-3">{{ post.value.text|richtext }}</div>
{% if post.value.url %}
<div>
<a href="{{ post.value.url }}" class="btn btn-primary"><i class="bi bi-plus"></i> Mehr Infos</a>
</div>
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
<hr class="mb-4">
{% endfor %}
</div>
{% endblock content %} {% endblock content %}

@ -1,55 +1,90 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/timeline.css' %}">
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
{% for event in self.events %} <ul class="timeline">
<div class="card mb-4 border-0 shadow-sm"> {% regroup self.get_all_events by value.start_date|date:"F Y" as events_by_month %}
<div class="card-header bg-light"> {% for month_group in events_by_month %}
<div class="d-flex justify-content-between align-items-center"> <li class="timeline-month-header">
<div> <h3 class="text-primary mb-4">{{ month_group.grouper }}</h3>
<span class="fs-5">{{ event.value.start_date|date:"SHORT_DATE_FORMAT" }}{% if event.value.end_date %} - {{ event.value.end_date|date:"SHORT_DATE_FORMAT" }}{% endif %}</span> </li>
</div> {% for event in month_group.list %}
<div> <li class="timeline-item clearfix">
<span class="text-muted">{{ event.value.start_date|date:"F Y" }}</span> <div class="timeline-badge">
</div> <span class="timeline-badge-day">{{ event.value.start_date|date:"d" }}</span>
<span class="timeline-badge-month">{{ event.value.start_date|date:"b" }}</span>
</div> </div>
</div> <div class="timeline-panel card border-0 shadow-sm">
<div class="card-body"> <div class="card-header bg-light">
<h3 class="card-title mb-3">{{ event.value.title }} <div class="d-flex justify-content-between align-items-center">
{% if event.value.subtitle %}<br><small class="text-muted">{{ event.value.subtitle }}</small>{% endif %}</h3> <div>
<div class="row"> <span class="fs-5">{{ event.value.start_date|date:"SHORT_DATE_FORMAT" }}{% if event.value.end_date %} - {{ event.value.end_date|date:"SHORT_DATE_FORMAT" }}{% endif %}</span>
<div class="col-md-4 mb-3 mb-md-0"> </div>
{% if event.value.image %} <div>
{% image event.value.image max-250x250 class="img-fluid rounded" %} <span class="text-muted">{{ event.value.start_date|date:"F Y" }}</span>
{% endif %} </div>
</div>
</div> </div>
<div class="col-md-4 mb-3 mb-md-0"> <div class="card-body">
<h5 class="mb-3"> <h3 class="card-title mb-3">
{{ event.value.start_date }}{% if event.value.end_date %} - {{ event.value.end_date }}{% endif %} {% if event.type == 'page' %}
</h5> <a href="{% pageurl event.page %}">{{ event.value.title }}</a>
{% else %}
{{ event.value.title }}
{% endif %}
{% if event.value.subtitle %}<br><small class="text-muted">{{ event.value.subtitle }}</small>{% endif %}
</h3>
<div class="row">
<div class="col-md-4 mb-3 mb-md-0">
{% if event.value.image %}
{% image event.value.image max-250x250 class="img-fluid rounded" %}
{% endif %}
</div>
<div class="col-md-4 mb-3 mb-md-0">
<h5 class="mb-3">
{{ event.value.start_date }}{% if event.value.end_date %} - {{ event.value.end_date }}{% endif %}
</h5>
<p> <p>
{% if event.value.location_name %}<strong>{{ event.value.location_name }}</strong><br>{% endif %} {% if event.value.location_name %}<strong>{{ event.value.location_name }}</strong><br>{% endif %}
{% if event.value.location_city %}{{ event.value.location_city }}{% endif %} {% if event.value.location_city %}{{ event.value.location_city }}{% endif %}
</p> </p>
</div>
<div class="col-md-4">
{% if event.type == 'manual' %}
{% if event.value.flyer.url %}
<p>
<a class="btn btn-outline-primary w-100 mb-2" href="{{ event.value.flyer.url }}" target="_blank"><i class="bi bi-cloud-download"></i> Programm herunterladen</a>
</p>
{% endif %}
{% if event.value.additional_info_target.url %}
<p>
<a class="btn btn-outline-primary w-100 mb-2" href="{{ event.value.additional_info_target.url }}"><i class="bi bi-info-circle"></i>
{{ event.value.additional_info_btn_label }}</a>
</p>
{% endif %}
{% elif event.type == 'page' %}
{% if event.value.flyer %}
<p>
<a class="btn btn-outline-primary w-100 mb-2" href="{{ event.value.flyer.url }}" target="_blank"><i class="bi bi-cloud-download"></i> Programm herunterladen</a>
</p>
{% endif %}
<p>
<a class="btn btn-outline-primary w-100 mb-2" href="{% pageurl event.page %}"><i class="bi bi-info-circle"></i> Details anzeigen</a>
</p>
{% endif %}
</div>
</div>
</div> </div>
<div class="col-md-4"> </div>
{% if event.value.flyer.url %} </li>
<p> {% endfor %}
<a class="btn btn-outline-primary w-100 mb-2" href="{{ event.value.flyer.url }}" target="_blank"><i class="bi bi-cloud-download"></i> Programm herunterladen</a> {% endfor %}
</p> </ul>
{% endif %}
{% if event.value.additional_info_target.url %}
<p>
<a class="btn btn-outline-primary w-100 mb-2" href="{{ event.value.additional_info_target.url }}"><i class="bi bi-info-circle"></i>
{{ event.value.additional_info_btn_label }}</a>
</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div> </div>
{% endblock content %} {% endblock content %}

@ -2,104 +2,7 @@
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %}
{% block extra_css %} {% block extra_css %}
<style> <link rel="stylesheet" href="{% static 'css/timeline.css' %}">
/* Timeline styling */
.timeline {
position: relative;
padding: 0;
list-style: none;
}
.timeline:before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 40px;
width: 4px;
background-color: var(--bs-primary);
}
.timeline-item {
position: relative;
margin-bottom: 50px;
}
.timeline-badge {
position: absolute;
top: 0;
left: 40px;
width: 60px;
height: 60px;
margin-left: -30px;
border-radius: 50%;
text-align: center;
background-color: var(--bs-primary);
color: white;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.timeline-badge-day {
font-size: 1.25rem;
font-weight: bold;
line-height: 1;
}
.timeline-badge-month {
font-size: 0.8rem;
text-transform: uppercase;
line-height: 1;
}
.timeline-month-header {
position: relative;
margin-bottom: 30px;
list-style: none;
}
.timeline-month-header h3 {
position: relative;
padding-left: 60px;
border-bottom: 2px solid var(--bs-primary);
padding-bottom: 10px;
}
.timeline-panel {
position: relative;
width: calc(100% - 90px);
float: right;
border-radius: 8px;
}
.timeline-panel:before {
content: '';
position: absolute;
top: 26px;
left: -15px;
border-top: 15px solid transparent;
border-right: 15px solid #fff;
border-bottom: 15px solid transparent;
}
.timeline-date {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: var(--bs-primary);
}
@media (max-width: 767px) {
.timeline-badge {
margin-left: 0;
}
}
</style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

@ -1,210 +1,345 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags crispy_forms_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags crispy_forms_tags %}
{% block extra_css %}
<style>
/* Event page specific styles - Hero sections moved to theme.css */
/* event-date-card styles moved to theme.css */
/* Image gallery and cards moved to theme.css */
/* Content sections moved to theme.css */
/* Tabs styling moved to theme.css */
/* Sponsor styling moved to theme.css */
/* Event page specific styles - common components moved to theme.css */
/* Responsive adjustments moved to theme.css */
</style>
{% endblock %}
{% block content %} {% block content %}
<!-- Hero Section with Carousel -->
{% if self.slide1_img %} {% if self.slide1_img %}
<div id="eventCarousel" class="carousel slide mb-4" data-bs-ride="carousel"> <div class="event-hero">
<div class="carousel-indicators"> <div id="eventCarousel" class="carousel slide event-hero-carousel" data-bs-ride="carousel">
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> <div class="carousel-indicators">
{% if self.slide2_img %} <button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button> {% if self.slide2_img %}
{% endif %} <button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>
{% if self.slide3_img %} {% endif %}
<button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button> {% if self.slide3_img %}
{% endif %} <button type="button" data-bs-target="#eventCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
{% image self.slide1_img fill-2000x500 class="d-block w-100" alt=self.slide1_headline %}
{% if self.slide1_headline or self.slide1_subline %}
<div class="carousel-caption d-none d-md-block">
{% if self.slide1_headline %}
<h2>{{ self.slide1_headline }}</h2>
{% endif %}
{% if self.slide1_subline %}
<p>{{ self.slide1_subline }}</p>
{% endif %}
{% if self.slide1_link_url and self.slide1_link_text %}
<a href="{{ self.slide1_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide1_link_text }}</a>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
{% if self.slide2_img %} <div class="carousel-inner">
<div class="carousel-item"> <div class="carousel-item active">
{% image self.slide2_img fill-2000x500 class="d-block w-100" alt=self.slide2_headline %} {% image self.slide1_img fill-2000x500 class="d-block w-100" alt=self.slide1_headline %}
{% if self.slide2_headline or self.slide2_subline %} {% if self.slide1_headline or self.slide1_subline %}
<div class="carousel-caption d-none d-md-block"> <div class="carousel-caption">
{% if self.slide2_headline %} {% if self.slide1_headline %}
<h2>{{ self.slide2_headline }}</h2> <h2 class="display-4 fw-light">{{ self.slide1_headline }}</h2>
{% endif %} {% endif %}
{% if self.slide2_subline %} {% if self.slide1_subline %}
<p>{{ self.slide2_subline }}</p> <p class="lead">{{ self.slide1_subline }}</p>
{% endif %}
{% if self.slide1_link_url and self.slide1_link_text %}
<a href="{{ self.slide1_link_url }}" class="btn btn-outline-light btn-lg mt-3">
<i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide1_link_text }}
</a>
{% endif %}
</div>
{% endif %} {% endif %}
{% if self.slide2_link_url and self.slide2_link_text %} </div>
<a href="{{ self.slide2_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide2_link_text }}</a> {% if self.slide2_img %}
<div class="carousel-item">
{% image self.slide2_img fill-2000x500 class="d-block w-100" alt=self.slide2_headline %}
{% if self.slide2_headline or self.slide2_subline %}
<div class="carousel-caption">
{% if self.slide2_headline %}
<h2 class="display-4 fw-light">{{ self.slide2_headline }}</h2>
{% endif %}
{% if self.slide2_subline %}
<p class="lead">{{ self.slide2_subline }}</p>
{% endif %}
{% if self.slide2_link_url and self.slide2_link_text %}
<a href="{{ self.slide2_link_url }}" class="btn btn-outline-light btn-lg mt-3">
<i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide2_link_text }}
</a>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> {% if self.slide3_img %}
{% endif %} <div class="carousel-item">
{% if self.slide3_img %} {% image self.slide3_img fill-2000x500 class="d-block w-100" alt=self.slide3_headline %}
<div class="carousel-item"> {% if self.slide3_headline or self.slide3_subline %}
{% image self.slide3_img fill-2000x500 class="d-block w-100" alt=self.slide3_headline %} <div class="carousel-caption">
{% if self.slide3_headline or self.slide3_subline %} {% if self.slide3_headline %}
<div class="carousel-caption d-none d-md-block"> <h2 class="display-4 fw-light">{{ self.slide3_headline }}</h2>
{% if self.slide3_headline %} {% endif %}
<h2>{{ self.slide3_headline }}</h2> {% if self.slide3_subline %}
{% endif %} <p class="lead">{{ self.slide3_subline }}</p>
{% if self.slide3_subline %} {% endif %}
<p>{{ self.slide3_subline }}</p> {% if self.slide3_link_url and self.slide3_link_text %}
{% endif %} <a href="{{ self.slide3_link_url }}" class="btn btn-outline-light btn-lg mt-3">
{% if self.slide3_link_url and self.slide3_link_text %} <i class="bi bi-arrow-right-circle me-2"></i>{{ self.slide3_link_text }}
<a href="{{ self.slide3_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide3_link_text }}</a> </a>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %} <button class="carousel-control-prev" type="button" data-bs-target="#eventCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#eventCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div> </div>
<button class="carousel-control-prev" type="button" data-bs-target="#eventCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#eventCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div> </div>
{% endif %} {% endif %}
<!-- Event Header -->
<div class="event-title-container">
<h1 class="event-title display-4">{{ self.title }}</h1>
{% if self.subtitle %}
<h2 class="event-subtitle h3">{{ self.subtitle }}</h2>
{% endif %}
</div>
<div class="row"> <div class="row">
{% if self.img1_img or self.img2_img %} <!-- Event Images (Moved to left) -->
<div class="col-md-4 col-sm-5 d-none d-sm-block"> <div class="col-lg-4">
{% if self.img1_img %} {% if self.img1_img or self.img2_img %}
{% image self.img1_img max-400x400 as img1 %} <div class="event-image-gallery">
<div class="card mb-4"> {% if self.img1_img %}
{% image self.img1_img width-400 as img1 %}
{% image self.img1_img original as img1_orig %} {% image self.img1_img original as img1_orig %}
<a href="{{ img1_orig.url }}" data-bs-toggle="modal" data-bs-target="#imageModal{{ img1.id }}"> <div class="event-image-card mb-4">
<img src="{{ img1.url }}" width="{{ img1.width }}" <a href="{{ img1_orig.url }}" data-bs-toggle="modal" data-bs-target="#imageModal{{ img1.id }}" class="d-block">
height="{{ img1.height }}" alt="{{ img1.alt }}" class="img-fluid card-img-top" /> <img src="{{ img1.url }}" width="{{ img1.width }}"
</a> height="{{ img1.height }}" alt="{{ img1.alt }}" class="img-fluid card-img-top" />
{% if self.img1_caption %} </a>
<div class="card-body"> {% if self.img1_caption %}
<p class="card-text">{{ self.img1_caption }}</p> <div class="card-body">
</div> <p class="card-text">{{ self.img1_caption }}</p>
{% endif %}
</div>
<!-- Modal for image 1 -->
<div class="modal fade" id="imageModal{{ img1.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ img1.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel{{ img1.id }}">{{ self.img1_caption|default:"Bild" }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body text-center"> {% endif %}
<img src="{{ img1_orig.url }}" alt="{{ img1.alt }}" class="img-fluid" /> </div>
<!-- Modal for image 1 -->
<div class="modal fade" id="imageModal{{ img1.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ img1.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel{{ img1.id }}">{{ self.img1_caption|default:"Bild" }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="{{ img1_orig.url }}" alt="{{ img1.alt }}" class="img-fluid" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {% endif %}
{% endif %}
{% if self.img2_img %} {% if self.img2_img %}
{% image self.img2_img max-400x400 as img2 %} {% image self.img2_img width-400 as img2 %}
<div class="card mb-4">
{% image self.img2_img original as img2_orig %} {% image self.img2_img original as img2_orig %}
<a href="{{ img2_orig.url }}" data-bs-toggle="modal" data-bs-target="#imageModal{{ img2.id }}"> <div class="event-image-card mb-4">
<img src="{{ img2.url }}" width="{{ img2.width }}" <a href="{{ img2_orig.url }}" data-bs-toggle="modal" data-bs-target="#imageModal{{ img2.id }}" class="d-block">
height="{{ img2.height }}" alt="{{ img2.alt }}" class="img-fluid card-img-top" /> <img src="{{ img2.url }}" width="{{ img2.width }}"
</a> height="{{ img2.height }}" alt="{{ img2.alt }}" class="img-fluid card-img-top" />
{% if self.img2_caption %} </a>
<div class="card-body"> {% if self.img2_caption %}
<p class="card-text">{{ self.img2_caption }}</p> <div class="card-body">
</div> <p class="card-text">{{ self.img2_caption }}</p>
{% endif %}
</div>
<!-- Modal for image 2 -->
<div class="modal fade" id="imageModal{{ img2.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ img2.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel{{ img2.id }}">{{ self.img2_caption|default:"Bild" }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body text-center"> {% endif %}
<img src="{{ img2_orig.url }}" alt="{{ img2.alt }}" class="img-fluid" /> </div>
<!-- Modal for image 2 -->
<div class="modal fade" id="imageModal{{ img2.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ img2.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel{{ img2.id }}">{{ self.img2_caption|default:"Bild" }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="{{ img2_orig.url }}" alt="{{ img2.alt }}" class="img-fluid" />
</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div>
{% endif %}
</div>
<div class="col-lg-8">
<!-- Event Date Card -->
<div class="event-date-card">
<div class="row align-items-center">
<div class="col-md-7">
<h3 class="card-title mb-2">
<i class="bi bi-calendar-event me-2"></i>
{{ self.start_date }}{% if self.end_date %} - {{ self.end_date }}{% endif %}
</h3>
{% if self.location_name %}
<p class="mb-0">
<i class="bi bi-geo-alt me-2"></i>
{{ self.location_name }}{% if self.location_city %}, {{ self.location_city }}{% endif %}
</p>
{% endif %}
</div> </div>
{% endif %} <div class="col-md-5 text-md-end mt-3 mt-md-0">
</div> {% if self.is_registration_active %}
{% endif %} {% if self.pretix_slug %}
<div class="col-md-8 col-sm-7"> <a class="btn btn-light btn-lg" href="{{ self.pretix_url }}" target="_blank">
<div class="border-bottom mb-3"> <i class="bi bi-people me-2"></i>Jetzt anmelden
<h2>{{ self.title }}</h2> </a>
</div> {% elif self.registration_url %}
<h3 class="text-muted mb-4">{{ self.subtitle }}</h3> <a class="btn btn-light btn-lg" href="{{ self.registration_url }}" target="_blank">
<div class="card bg-light mb-4"> <i class="bi bi-people me-2"></i>Jetzt anmelden
<div class="card-body text-center"> </a>
<h3 class="card-title">{{ self.start_date }}{% if self.end_date %} - {{ self.end_date }}{% endif %}</h3> {% else %}
{% if self.is_registration_active %} {% endif %}
{% if self.pretix_slug %}
<a class="btn btn-primary btn-lg" href="{{ self.pretix_url }}" target="_blank"><i class="bi bi-people"></i> Jetzt anmelden</a>
{% elif self.registration_url %}
<a class="btn btn-primary btn-lg" href="{{ self.registration_url }}" target="_blank"><i class="bi bi-people"></i> Jetzt anmelden</a>
{% else %}
<button class="btn btn-primary btn-lg" data-bs-toggle="modal" data-bs-target="#registration-modal"><i class="bi bi-people"></i> Jetzt anmelden</button>
{% endif %} {% endif %}
{% endif %} </div>
</div> </div>
</div> </div>
<div class="mb-4">
<!-- Event Description -->
<div class="event-description">
{{ self.description|richtext }} {{ self.description|richtext }}
</div> </div>
{% if self.tabs %}
<div class="mb-4"> <!-- Event Details Cards -->
<ul class="nav nav-tabs" role="tablist"> <div class="row mb-4">
{% for tab in self.tabs %} {% if self.location_name %}
<li class="nav-item"> <div class="col-lg-4 col-md-6 col-12 mb-4">
<a class="nav-link {{ forloop.counter0|yesno:",active" }}" <div class="card h-100 border-0 shadow-sm event-detail-card">
id="tab-{{ forloop.counter }}-tab" <div class="card-body">
data-bs-toggle="tab" <h3 class="card-title mb-4">
href="#tab-{{ forloop.counter }}" <i class="bi bi-geo-alt-fill text-primary me-2"></i>Veranstaltungsort
role="tab" </h3>
aria-controls="tab-{{ forloop.counter }}" <address class="mb-0">
aria-selected="{{ forloop.counter0|yesno:"false,true" }}"> <strong>{{ self.location_name }}</strong><br>
{{ tab.value.title }} {% if self.location_street %}{{ self.location_street }}<br>{% endif %}
{% if self.location_city %}{{ self.location_city }}{% endif %}
</address>
</div>
</div>
</div>
{% endif %}
{% if self.flyer %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="card h-100 border-0 shadow-sm event-detail-card">
<div class="card-body">
<h3 class="card-title mb-4">
<i class="bi bi-file-earmark-text text-primary me-2"></i>Programm
</h3>
<a class="btn btn-outline-primary btn-lg w-100 mb-3" href="{{ self.flyer.url }}" target="_blank">
<i class="bi bi-cloud-download me-2"></i>Flyer herunterladen
</a>
<p class="small text-muted mb-0">
{{ self.flyer.title }}<br>
Stand: {{ self.flyer.created_at }}
</p>
</div>
</div>
</div>
{% endif %}
{% if self.downloads %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="card h-100 border-0 shadow-sm event-detail-card">
<div class="card-body">
<h3 class="card-title mb-4">
<i class="bi bi-download text-primary me-2"></i>Downloads
</h3>
{% for download in self.downloads %}
<a class="btn btn-outline-primary mb-3 w-100" href="{{ download.value.file.url }}" target="_blank">
<i class="bi bi-cloud-download me-2"></i>{{ download.value.title }}
</a> </a>
</li> <p class="small text-muted {% if not forloop.last %}mb-4{% else %}mb-0{% endif %}">
{% endfor %} {{ download.value.file.title }}<br>
</ul> Stand: {{ download.value.file.created_at }}
<div class="tab-content pt-3"> </p>
{% for tab in self.tabs %} {% endfor %}
<div class="tab-pane fade{{ forloop.counter0|yesno:", active show" }}" </div>
id="tab-{{ forloop.counter }}" </div>
role="tabpanel" </div>
aria-labelledby="tab-{{ forloop.counter }}-tab"> {% endif %}
{% if tab.block_type == 'richtext_tab' %} </div>
{{ tab.value.content|richtext }} </div>
{% elif tab.block_type == 'media_tab' %} </div>
<div class="mb-3">{{ tab.value.pre_content|richtext }}</div>
<div class="mb-3">{% include 'core/blocks/media_tab.html' with value=tab.value.media_file autoplay=tab.value.autoplay muted=tab.value.muted %}</div> <!-- Tabs Section -->
<div>{{ tab.value.post_content|richtext }}</div> {% if self.tabs %}
{% elif tab.block_type == 'sponsor_tab' %} <div class="event-tabs mb-4 mt-4">
{% for sponsor in tab.value.sponsors %} <ul class="nav nav-pills nav-fill mb-4" role="tablist">
{% if sponsor.block_type == 'headline' %} {% for tab in self.tabs %}
<h3 class="border-bottom pb-2 mb-3">{{ sponsor }}</h3> <li class="nav-item">
{% elif sponsor.block_type == 'sponsor' %} <a class="nav-link {{ forloop.counter0|yesno:",active" }}"
<div class="row mb-4"> id="tab-{{ forloop.counter }}-tab"
<div class="col-md-3 col-sm-5 col-12"> data-bs-toggle="tab"
href="#tab-{{ forloop.counter }}"
role="tab"
aria-controls="tab-{{ forloop.counter }}"
aria-selected="{{ forloop.counter0|yesno:"false,true" }}">
{{ tab.value.title }}
</a>
</li>
{% endfor %}
</ul>
<div class="tab-content p-4 bg-white rounded-3 shadow-sm">
{% for tab in self.tabs %}
<div class="tab-pane fade{{ forloop.counter0|yesno:", active show" }}"
id="tab-{{ forloop.counter }}"
role="tabpanel"
aria-labelledby="tab-{{ forloop.counter }}-tab">
{% if tab.block_type == 'richtext_tab' %}
{{ tab.value.content|richtext }}
{% elif tab.block_type == 'media_tab' %}
<div class="mb-4">{{ tab.value.pre_content|richtext }}</div>
<div class="mb-4 rounded overflow-hidden">
{% include 'core/blocks/media_tab.html' with value=tab.value.media_file autoplay=tab.value.autoplay muted=tab.value.muted %}
</div>
<div>{{ tab.value.post_content|richtext }}</div>
{% elif tab.block_type == 'image_tab' %}
<div class="image-tab-container text-{{ tab.value.alignment }}">
<div class="image-tab-wrapper image-size-{{ tab.value.size }}">
{% image tab.value.image original as img %}
<img src="{{ img.url }}" alt="{{ img.alt }}" class="img-fluid rounded" />
{% if tab.value.caption %}
<div class="image-tab-caption mt-2">{{ tab.value.caption }}</div>
{% endif %}
</div>
</div>
{% elif tab.block_type == 'sponsor_tab' %}
{% for sponsor in tab.value.sponsors %}
{% if sponsor.block_type == 'headline' %}
<h3 class="mb-4 pb-2 position-relative sponsor-heading">{{ sponsor }}</h3>
{% elif sponsor.block_type == 'sponsor' %}
<div class="card sponsor-card mb-4 border-0 shadow-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-3 col-sm-5 col-12 text-center mb-3 mb-md-0">
{% if sponsor.value.logo %} {% if sponsor.value.logo %}
{% image sponsor.value.logo max-250x250 as img %} {% image sponsor.value.logo max-250x250 as img %}
{% image sponsor.value.logo original as img_orig %} {% image sponsor.value.logo original as img_orig %}
<a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#sponsorModal{{ img.id }}"> <a href="{{ img_orig.url }}" data-bs-toggle="modal" data-bs-target="#sponsorModal{{ img.id }}" class="d-inline-block">
<img src="{{ img.url }}" width="{{ img.width }}" <img src="{{ img.url }}" width="{{ img.width }}"
height="{{ img.height }}" alt="{{ img.alt }}" class="img-fluid" /> height="{{ img.height }}" alt="{{ img.alt }}" class="img-fluid sponsor-logo" />
</a> </a>
<!-- Modal for sponsor logo --> <!-- Modal for sponsor logo -->
<div class="modal fade" id="sponsorModal{{ img.id }}" tabindex="-1" aria-labelledby="sponsorModalLabel{{ img.id }}" aria-hidden="true"> <div class="modal fade" id="sponsorModal{{ img.id }}" tabindex="-1" aria-labelledby="sponsorModalLabel{{ img.id }}" aria-hidden="true">
@ -223,112 +358,58 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-md-9 col-sm-7 col-12"> <div class="col-md-9 col-sm-7 col-12">
{% if sponsor.value.logo %} <h4 class="mb-2">
<div class="d-none d-md-block mb-4"></div> {% if sponsor.value.url %}
{% endif %} <a href="{{ sponsor.value.url }}" target="_blank" class="text-decoration-none">
{% if sponsor.value.url %} {{ sponsor.value.name }}
<p><a href="{{ sponsor.value.url }}" target="_blank">{{ sponsor.value.name }}</a></p> <i class="bi bi-box-arrow-up-right ms-2 small"></i>
{% else %} </a>
<p>{{ sponsor.value.name }}</p> {% else %}
{% endif %} {{ sponsor.value.name }}
{% endif %}
</h4>
</div> </div>
</div> </div>
{% endif %} </div>
{% endfor %} </div>
{% endif %} {% endif %}
</div> {% endfor %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="row mb-4">
{% if self.location_name %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="border-bottom pb-2 mb-3">
<h3>Veranstaltungsort</h3>
</div>
<address class="mb-4">
<strong>{{ self.location_name }}</strong><br>
{% if self.location_street %}{{ self.location_street }}<br>{% endif %}
{% if self.location_city %}{{ self.location_city }}{% endif %}
</address>
</div>
{% endif %}
{% if self.flyer %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="border-bottom pb-2 mb-3">
<h3>Programm</h3>
</div>
<a class="btn btn-primary mb-2" href="{{ self.flyer.url }}" target="_blank"><i class="bi bi-cloud-download"></i> Flyer herunterladen</a>
<p>{{ self.flyer.title}}<br>
Stand: {{ self.flyer.created_at }}</p>
</div>
{% endif %}
{% if self.downloads %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="border-bottom pb-2 mb-3">
<h3>Downloads</h3>
</div>
{% for download in self.downloads %}
<a class="btn btn-primary mb-2 d-block" href="{{ download.value.file.url }}" target="_blank"><i class="bi bi-cloud-download"></i> {{ download.value.title }}</a>
<p>{{ download.value.file.title }}<br>
Stand: {{ download.value.file.created_at }}</p>
{% endfor %}
</div>
{% endif %}
{% if self.is_registration_active %}
<div class="col-lg-4 col-md-6 col-12 mb-4">
<div class="border-bottom pb-2 mb-3">
<h3>Online Anmeldung</h3>
</div>
{% if self.pretix_slug %}
<a class="btn btn-primary" href="{{ self.pretix_url }}" target="_blank"><i class="bi bi-people"></i> Jetzt anmelden</a>
{% elif self.registration_url %}
<a class="btn btn-primary" href="{{ self.registration_url }}" target="_blank"><i class="bi bi-people"></i> Jetzt anmelden</a>
{% else %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registration-modal"><i class="bi bi-people"></i> Jetzt anmelden</button>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endfor %}
</div> </div>
</div> </div>
</div> {% endif %}
{% if self.is_registration_active and not self.pretix_slug %}
<div class="modal fade" id="registration-modal" tabindex="-1" aria-labelledby="registrationModalLabel" aria-hidden="true"> <!-- Event CTA Section -->
<div class="modal-dialog modal-lg"> {% if self.is_registration_active %}
<div class="modal-content"> <div class="event-cta mt-5 mb-5 p-4 p-md-5 text-center text-md-start">
<div class="modal-header"> <div class="row align-items-center">
<h5 class="modal-title" id="registrationModalLabel">Online-Anmeldung {{ self.title }}</h5> <div class="col-md-8 mb-4 mb-md-0">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <h2 class="text-white mb-3">Jetzt für "{{ self.title }}" anmelden!</h2>
</div> <p class="text-white-75 mb-0">Sichern Sie sich Ihren Platz bei dieser Veranstaltung und profitieren Sie von spannenden Vorträgen und wertvollen Networking-Möglichkeiten.</p>
{% if self.external_registration_code %} </div>
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;"> <div class="col-md-4 text-center text-md-end">
{{ self.external_registration_code|safe }} {% if self.is_registration_active %}
</div> {% if self.pretix_slug %}
<a class="btn btn-light btn-lg" href="{{ self.pretix_url }}" target="_blank">
<i class="bi bi-people me-2"></i>Jetzt anmelden
</a>
{% elif self.registration_url %}
<a class="btn btn-light btn-lg" href="{{ self.registration_url }}" target="_blank">
<i class="bi bi-people me-2"></i>Jetzt anmelden
</a>
{% else %} {% else %}
<form action="{% pageurl self %}" method="POST">
<div class="modal-body">
{% csrf_token %}
{% crispy form self.get_form_helper %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">{{ self.is_free|yesno:"Verbindlich,Kostenpflichtig" }} anmelden</button>
</div>
</form>
{% endif %} {% endif %}
</div> {% endif %}
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}
{% block ready_js %} {% block extra_js %}
{{ block.super }} {{ block.super }}
{% if form.errors %}
var myModal = new bootstrap.Modal(document.getElementById('registration-modal'));
myModal.show();
{% endif %}
{% endblock %} {% endblock %}

@ -1,23 +1,255 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %}
{% block extra_css %}
<style>
/* Hero section styles */
.hero-carousel .carousel-item {
height: 80vh;
min-height: 500px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.hero-carousel .carousel-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.6));
}
.hero-carousel .carousel-caption {
bottom: 20%;
max-width: 700px;
margin: 0 auto;
padding: 2rem;
border-radius: 8px;
background-color: rgba(0,0,0,0.4);
backdrop-filter: blur(5px);
animation: fadeInUp 1s ease-out;
}
.hero-carousel h2 {
font-size: 3rem;
font-weight: 300;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.hero-carousel p {
font-size: 1.25rem;
margin-bottom: 1.5rem;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
.hero-carousel .btn {
padding: 0.75rem 2rem;
font-size: 1.1rem;
border-width: 2px;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
}
.hero-carousel .btn:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Coming up section styles */
.coming-up-section {
margin-top: -80px;
position: relative;
z-index: 10;
}
.coming-up-card {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border: none;
}
.coming-up-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.coming-up-header {
background-color: var(--bs-primary);
color: white;
padding: 1rem 1.5rem;
}
.coming-up-badge {
background-color: white;
color: var(--bs-primary);
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 50px;
display: inline-block;
margin-bottom: 0.5rem;
}
.event-date-badge {
width: 80px;
height: 80px;
background-color: var(--bs-primary);
color: white;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
box-shadow: 0 5px 15px rgba(var(--bs-primary-rgb), 0.3);
}
.event-date-day {
font-size: 2rem;
font-weight: bold;
line-height: 1;
}
.event-date-month {
font-size: 1rem;
text-transform: uppercase;
}
/* Feature cards */
.feature-card {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
transition: all 0.3s ease;
border: none;
height: 100%;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.feature-card .card-img-top {
height: 220px;
object-fit: cover;
}
.feature-card .card-body {
padding: 1.5rem;
}
.feature-card .card-title {
font-size: 1.4rem;
margin-bottom: 1rem;
font-weight: 400;
}
.feature-card .card-text {
color: #666;
}
.feature-card .card-footer {
background-color: transparent;
border-top: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
}
/* Welcome section */
.welcome-section {
padding: 5rem 0;
}
.section-title {
position: relative;
margin-bottom: 3rem;
text-align: center;
}
.section-title:after {
content: '';
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 3px;
background-color: var(--bs-primary);
border-radius: 3px;
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out;
}
/* Responsive adjustments */
@media (max-width: 991.98px) {
.hero-carousel .carousel-item {
height: 60vh;
}
.hero-carousel h2 {
font-size: 2.5rem;
}
.coming-up-section {
margin-top: 2rem;
}
}
@media (max-width: 767.98px) {
.hero-carousel .carousel-item {
height: 50vh;
}
.hero-carousel h2 {
font-size: 2rem;
}
.hero-carousel p {
font-size: 1rem;
}
}
</style>
{% endblock %}
{% block fullwidth_header %} {% block fullwidth_header %}
{% if self.slide1_img %} {% if self.slide1_img %}
<div id="homeCarousel" class="carousel slide" data-bs-ride="carousel"> <div id="heroCarousel" class="carousel slide hero-carousel" data-bs-ride="carousel">
<div class="carousel-indicators"> <div class="carousel-indicators">
<button type="button" data-bs-target="#homeCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
{% if self.slide2_img %} {% if self.slide2_img %}
<button type="button" data-bs-target="#homeCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button> <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>
{% endif %} {% endif %}
{% if self.slide3_img %} {% if self.slide3_img %}
<button type="button" data-bs-target="#homeCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button> <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>
{% endif %} {% endif %}
</div> </div>
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item active"> <div class="carousel-item active">
{% image self.slide1_img fill-2000x500 class="d-block w-100" alt=self.slide1_headline %} {% image self.slide1_img fill-2000x1200 as slide1 %}
<div class="carousel-item-bg" style="background-image: url('{{ slide1.url }}'); height: 80vh; min-height: 500px; background-size: cover; background-position: center;"></div>
{% if self.slide1_headline or self.slide1_subline %} {% if self.slide1_headline or self.slide1_subline %}
<div class="carousel-caption d-none d-md-block"> <div class="carousel-caption d-md-block">
{% if self.slide1_headline %} {% if self.slide1_headline %}
<h2>{{ self.slide1_headline }}</h2> <h2>{{ self.slide1_headline }}</h2>
{% endif %} {% endif %}
@ -25,16 +257,17 @@
<p>{{ self.slide1_subline }}</p> <p>{{ self.slide1_subline }}</p>
{% endif %} {% endif %}
{% if self.slide1_link_url and self.slide1_link_text %} {% if self.slide1_link_url and self.slide1_link_text %}
<a href="{{ self.slide1_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide1_link_text }}</a> <a href="{{ self.slide1_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide1_link_text }} <i class="bi bi-arrow-right"></i></a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if self.slide2_img %} {% if self.slide2_img %}
<div class="carousel-item"> <div class="carousel-item">
{% image self.slide2_img fill-2000x500 class="d-block w-100" alt=self.slide2_headline %} {% image self.slide2_img fill-2000x1200 as slide2 %}
<div class="carousel-item-bg" style="background-image: url('{{ slide2.url }}'); height: 80vh; min-height: 500px; background-size: cover; background-position: center;"></div>
{% if self.slide2_headline or self.slide2_subline %} {% if self.slide2_headline or self.slide2_subline %}
<div class="carousel-caption d-none d-md-block"> <div class="carousel-caption d-md-block">
{% if self.slide2_headline %} {% if self.slide2_headline %}
<h2>{{ self.slide2_headline }}</h2> <h2>{{ self.slide2_headline }}</h2>
{% endif %} {% endif %}
@ -42,7 +275,7 @@
<p>{{ self.slide2_subline }}</p> <p>{{ self.slide2_subline }}</p>
{% endif %} {% endif %}
{% if self.slide2_link_url and self.slide2_link_text %} {% if self.slide2_link_url and self.slide2_link_text %}
<a href="{{ self.slide2_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide2_link_text }}</a> <a href="{{ self.slide2_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide2_link_text }} <i class="bi bi-arrow-right"></i></a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -50,9 +283,10 @@
{% endif %} {% endif %}
{% if self.slide3_img %} {% if self.slide3_img %}
<div class="carousel-item"> <div class="carousel-item">
{% image self.slide3_img fill-2000x500 class="d-block w-100" alt=self.slide3_headline %} {% image self.slide3_img fill-2000x1200 as slide3 %}
<div class="carousel-item-bg" style="background-image: url('{{ slide3.url }}'); height: 80vh; min-height: 500px; background-size: cover; background-position: center;"></div>
{% if self.slide3_headline or self.slide3_subline %} {% if self.slide3_headline or self.slide3_subline %}
<div class="carousel-caption d-none d-md-block"> <div class="carousel-caption d-md-block">
{% if self.slide3_headline %} {% if self.slide3_headline %}
<h2>{{ self.slide3_headline }}</h2> <h2>{{ self.slide3_headline }}</h2>
{% endif %} {% endif %}
@ -60,18 +294,18 @@
<p>{{ self.slide3_subline }}</p> <p>{{ self.slide3_subline }}</p>
{% endif %} {% endif %}
{% if self.slide3_link_url and self.slide3_link_text %} {% if self.slide3_link_url and self.slide3_link_text %}
<a href="{{ self.slide3_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide3_link_text }}</a> <a href="{{ self.slide3_link_url }}" class="btn btn-outline-light mt-3">{{ self.slide3_link_text }} <i class="bi bi-arrow-right"></i></a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<button class="carousel-control-prev" type="button" data-bs-target="#homeCarousel" data-bs-slide="prev"> <button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span> <span class="visually-hidden">Previous</span>
</button> </button>
<button class="carousel-control-next" type="button" data-bs-target="#homeCarousel" data-bs-slide="next"> <button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span> <span class="visually-hidden">Next</span>
</button> </button>
@ -80,85 +314,158 @@
{% endblock fullwidth_header %} {% endblock fullwidth_header %}
{% block content %} {% block content %}
<div class="container"> <!-- Coming Up Section -->
<div class="row mb-5"> {% get_upcoming_events 1 as upcoming_events %}
<div class="col-12 text-center"> {% if upcoming_events %}
{{ self.block1|richtext }} {% with upcoming_event=upcoming_events.0 %}
</div> <div class="coming-up-section">
</div> <div class="container">
<div class="row"> <div class="row">
{% if self.thumbnail1_img %} <div class="col-12">
<div class="col-12 col-sm-6 col-md-4 mb-4"> <div class="card coming-up-card animate-fade-in-up">
<div class="card h-100"> <div class="coming-up-header">
<div class="card-img-top"> <div class="d-flex justify-content-between align-items-center">
{% image self.thumbnail1_img fill-600x400-c100 class="img-fluid" %} <div>
</div> <span class="coming-up-badge"><i class="bi bi-calendar-event"></i> Nächste Veranstaltung</span>
<div class="card-body"> <h3 class="mb-0">{{ upcoming_event.title }}</h3>
<h3 class="card-title h5"> </div>
{% if self.thumbnail1_more_page %} <a href="{{ upcoming_event.url }}" class="btn btn-light">Details <i class="bi bi-arrow-right"></i></a>
<a href="{% pageurl self.thumbnail1_more_page %}" class="text-decoration-none">{{ self.thumbnail1_title }}</a> </div>
{% else %} </div>
{{ self.thumbnail1_title }} <div class="card-body">
{% endif %} <div class="row align-items-center">
</h3> <div class="col-md-2 mb-3 mb-md-0 text-center">
<div class="card-text">{{ self.thumbnail1_text|richtext }}</div> <div class="event-date-badge mx-auto">
<span class="event-date-day">{{ upcoming_event.start_date|date:"d" }}</span>
<span class="event-date-month">{{ upcoming_event.start_date|date:"b" }}</span>
</div>
</div>
<div class="col-md-7 mb-3 mb-md-0">
{% if upcoming_event.subtitle %}<p class="text-muted mb-2">{{ upcoming_event.subtitle }}</p>{% endif %}
<p class="mb-2"><i class="bi bi-geo-alt"></i>
{% if upcoming_event.location_name %}{{ upcoming_event.location_name }}{% endif %}
{% if upcoming_event.location_city %}, {{ upcoming_event.location_city }}{% endif %}
</p>
<p class="mb-0"><i class="bi bi-clock"></i>
{{ upcoming_event.start_date|date:"SHORT_DATE_FORMAT" }}
{% if upcoming_event.end_date %} - {{ upcoming_event.end_date|date:"SHORT_DATE_FORMAT" }}{% endif %}
</p>
</div>
<div class="col-md-3 text-md-end">
{% if upcoming_event.is_registration_active %}
<a href="{{ upcoming_event.url }}" class="btn btn-primary mb-2 w-100"><i class="bi bi-people"></i> Anmelden</a>
{% endif %}
{% if upcoming_event.flyer.url %}
<a href="{{ upcoming_event.flyer.url }}" class="btn btn-outline-primary w-100" target="_blank"><i class="bi bi-file-earmark-pdf"></i> Programm</a>
{% endif %}
</div>
</div>
</div>
</div> </div>
{% if self.thumbnail1_more_page %}
<div class="card-footer bg-transparent border-top-0">
<a href="{% pageurl self.thumbnail1_more_page %}" class="btn btn-outline-primary">{{ self.thumbnail1_more_text|default:'Mehr Info' }}</a>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} </div>
{% if self.thumbnail2_img %} </div>
<div class="col-12 col-sm-6 col-md-4 mb-4"> {% endwith %}
<div class="card h-100"> {% endif %}
<div class="card-img-top">
{% image self.thumbnail2_img fill-600x400-c100 class="img-fluid" %} <!-- Welcome Section -->
</div> <div class="welcome-section">
<div class="card-body"> <div class="container">
<h3 class="card-title h5"> <div class="row justify-content-center">
{% if self.thumbnail2_more_page %} <div class="col-lg-10 text-center animate-fade-in-up">
<a href="{% pageurl self.thumbnail2_more_page %}" class="text-decoration-none">{{ self.thumbnail2_title }}</a> <h2 class="section-title">Willkommen bei FEO GmbH</h2>
{% else %} {{ self.block1|richtext }}
{{ self.thumbnail2_title }}
{% endif %}
</h3>
<div class="card-text">{{ self.thumbnail2_text|richtext }}</div>
</div>
{% if self.thumbnail2_more_page %}
<div class="card-footer bg-transparent border-top-0">
<a href="{% pageurl self.thumbnail2_more_page %}" class="btn btn-outline-primary">{{ self.thumbnail2_more_text|default:'Mehr Info' }}</a>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} </div>
{% if self.thumbnail3_img %} </div>
<div class="col-12 col-sm-6 col-md-4 mb-4">
<div class="card h-100"> <!-- Features Section -->
<div class="card-img-top"> <div class="features-section py-5">
{% image self.thumbnail3_img fill-600x400-c100 class="img-fluid" %} <div class="container">
<div class="row">
{% if self.thumbnail1_img %}
<div class="col-12 col-md-4 mb-4 animate-fade-in-up" style="animation-delay: 0.1s;">
<div class="feature-card">
<div class="card-img-top">
{% image self.thumbnail1_img fill-600x400-c100 class="img-fluid w-100 h-100" style="object-fit: cover;" %}
</div>
<div class="card-body">
<h3 class="card-title">
{% if self.thumbnail1_more_page %}
<a href="{% pageurl self.thumbnail1_more_page %}" class="text-decoration-none">{{ self.thumbnail1_title }}</a>
{% else %}
{{ self.thumbnail1_title }}
{% endif %}
</h3>
<div class="card-text">{{ self.thumbnail1_text|richtext }}</div>
</div>
{% if self.thumbnail1_more_page %}
<div class="card-footer">
<a href="{% pageurl self.thumbnail1_more_page %}" class="btn btn-outline-primary">
{{ self.thumbnail1_more_text|default:'Mehr Info' }} <i class="bi bi-arrow-right"></i>
</a>
</div>
{% endif %}
</div> </div>
<div class="card-body"> </div>
<h3 class="card-title h5"> {% endif %}
{% if self.thumbnail3_more_page %}
<a href="{% pageurl self.thumbnail3_more_page %}" class="text-decoration-none">{{ self.thumbnail3_title }}</a> {% if self.thumbnail2_img %}
{% else %} <div class="col-12 col-md-4 mb-4 animate-fade-in-up" style="animation-delay: 0.2s;">
{{ self.thumbnail3_title }} <div class="feature-card">
{% endif %} <div class="card-img-top">
</h3> {% image self.thumbnail2_img fill-600x400-c100 class="img-fluid w-100 h-100" style="object-fit: cover;" %}
<div class="card-text">{{ self.thumbnail3_text|richtext }}</div> </div>
<div class="card-body">
<h3 class="card-title">
{% if self.thumbnail2_more_page %}
<a href="{% pageurl self.thumbnail2_more_page %}" class="text-decoration-none">{{ self.thumbnail2_title }}</a>
{% else %}
{{ self.thumbnail2_title }}
{% endif %}
</h3>
<div class="card-text">{{ self.thumbnail2_text|richtext }}</div>
</div>
{% if self.thumbnail2_more_page %}
<div class="card-footer">
<a href="{% pageurl self.thumbnail2_more_page %}" class="btn btn-outline-primary">
{{ self.thumbnail2_more_text|default:'Mehr Info' }} <i class="bi bi-arrow-right"></i>
</a>
</div>
{% endif %}
</div> </div>
{% if self.thumbnail3_more_page %} </div>
<div class="card-footer bg-transparent border-top-0"> {% endif %}
<a href="{% pageurl self.thumbnail3_more_page %}" class="btn btn-outline-primary">{{ self.thumbnail3_more_text|default:'Mehr Info' }}</a>
{% if self.thumbnail3_img %}
<div class="col-12 col-md-4 mb-4 animate-fade-in-up" style="animation-delay: 0.3s;">
<div class="feature-card">
<div class="card-img-top">
{% image self.thumbnail3_img fill-600x400-c100 class="img-fluid w-100 h-100" style="object-fit: cover;" %}
</div>
<div class="card-body">
<h3 class="card-title">
{% if self.thumbnail3_more_page %}
<a href="{% pageurl self.thumbnail3_more_page %}" class="text-decoration-none">{{ self.thumbnail3_title }}</a>
{% else %}
{{ self.thumbnail3_title }}
{% endif %}
</h3>
<div class="card-text">{{ self.thumbnail3_text|richtext }}</div>
</div>
{% if self.thumbnail3_more_page %}
<div class="card-footer">
<a href="{% pageurl self.thumbnail3_more_page %}" class="btn btn-outline-primary">
{{ self.thumbnail3_more_text|default:'Mehr Info' }} <i class="bi bi-arrow-right"></i>
</a>
</div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

@ -1,30 +1,33 @@
{% load menu_tags wagtailcore_tags static %} {% load menu_tags wagtailcore_tags static %}
{% get_site_root as site_root %} {% get_site_root as site_root %}
<nav class="navbar navbar-expand-lg navbar-light bg-white py-3"> <nav class="navbar navbar-expand-lg navbar-light bg-white py-3 sticky-top modern-navbar">
<div class="container"> <div class="container">
<a class="navbar-brand" href="{{ site_root.url }}"> <a class="navbar-brand" href="{{ site_root.url }}">
<img src="{% static 'img/logo_feo.png' %}" alt="Logo FEO GmbH" height="60"> <img src="{% static 'img/logo_feo.png' %}" alt="Logo FEO GmbH" height="50" class="logo-img">
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
{% for menuitem in menuitems %} {% for menuitem in menuitems %}
{% if menuitem.show_dropdown %} {% if menuitem.show_dropdown %}
<li class="nav-item dropdown{% if menuitem.active %} active{% endif %}"> <li class="nav-item dropdown{% if menuitem.active %} active{% endif %} mx-1">
<a class="nav-link dropdown-toggle{% if menuitem.active %} text-primary{% endif %}" href="#" id="navbarDropdown{{ forloop.counter }}" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle{% if menuitem.active %} active-link{% endif %}" href="#" id="navbarDropdown{{ forloop.counter }}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ menuitem.title }} {{ menuitem.title }}
</a> </a>
{% bootstrap5_top_menu_children parent=menuitem %} {% bootstrap5_top_menu_children parent=menuitem %}
</li> </li>
{% else %} {% else %}
<li class="nav-item{% if menuitem.active %} active{% endif %}"> <li class="nav-item{% if menuitem.active %} active{% endif %} mx-1">
<a class="nav-link{% if menuitem.active %} text-primary{% endif %}" href="{% pageurl menuitem %}">{{ menuitem.title }}</a> <a class="nav-link{% if menuitem.active %} active-link{% endif %}" href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<li class="nav-item ms-lg-2">
<a href="{% slugurl 'contact' %}" class="btn btn-primary nav-btn">Kontakt</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

@ -1,9 +1,8 @@
{% load menu_tags wagtailcore_tags %} {% load menu_tags wagtailcore_tags %}
<ul class="dropdown-menu shadow-sm border-0" aria-labelledby="navbarDropdown"> <ul class="dropdown-menu modern-dropdown shadow border-0" aria-labelledby="navbarDropdown">
{# Include link to parent because the parent link is a drop down #} <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% pageurl parent %}">{{ parent.title }}</a></li>
{% for child in menuitems_children %} {% for child in menuitems_children %}
<li><a class="dropdown-item{% if child.active %} active text-primary{% endif %}" href="{% pageurl child %}">{{ child.title }}</a></li> <li><a class="dropdown-item{% if child.active %} active{% endif %}" href="{% pageurl child %}">{{ child.title }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>

@ -1,27 +1,42 @@
{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %} {% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %}
<div class="border-bottom pb-2 mb-3"> <div class="event-calendar">
<h2>Veranstaltungskalender</h2> <div class="event-calendar-header mb-3">
</div> <h2 class="event-calendar-title">Veranstaltungskalender</h2>
<div class="mb-4 overflow-auto" style="max-height: 300px;"> </div>
{% for event in events %} <div class="event-list mb-4 overflow-auto" style="max-height: 300px; overflow-x: hidden !important;">
<div class="mb-3 pb-3 border-bottom"> {% for event in events %}
<div class="d-flex"> <div class="event-item mb-3">
<div class="text-center me-3 bg-light rounded p-2"> <div class="d-flex">
<span class="d-block fs-4">{{ event.start_date|date:"d" }}</span> <div class="event-date-badge me-3">
<small class="text-muted">{{ event.start_date|date:"M Y" }}</small> <span class="event-day">{{ event.start_date|date:"d" }}</span>
</div> <span class="event-month">{{ event.start_date|date:"M" }}</span>
<div class="overflow-hidden"> <span class="event-year">{{ event.start_date|date:"Y" }}</span>
<h3 class="h5"><a href="{% pageurl event %}" class="text-decoration-none">{{ event.title }}</a></h3> </div>
<p class="mb-0"> <div class="event-content">
{{ event.start_date }}{% if event.end_date %} - {{ event.end_date }}{% endif %} <h3 class="event-title"><a href="{% pageurl event %}" class="event-link">{{ event.title }}</a></h3>
{{ event.location_name }}<br> <div class="event-details">
{{ event.location_city }}</p> <div class="event-date">
<i class="bi bi-calendar-event"></i>
{{ event.start_date|date:"d.m.Y" }}{% if event.end_date %} - {{ event.end_date|date:"d.m.Y" }}{% endif %}
</div>
{% if event.location_name or event.location_city %}
<div class="event-location">
<i class="bi bi-geo-alt"></i>
{% if event.location_name %}{{ event.location_name }}{% endif %}
{% if event.location_name and event.location_city %}, {% endif %}
{% if event.location_city %}{{ event.location_city }}{% endif %}
</div>
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div>
<div class="event-calendar-footer text-end">
<a class="btn btn-outline-primary btn-sm" href="{% url 'ics_events' %}">
<i class="bi bi-calendar-plus"></i> Veranstaltungskalender als ICS
</a>
</div> </div>
{% endfor %}
</div>
<div class="pt-3 text-end">
<a class="btn btn-outline-primary btn-sm" href="https://feo.gmbh/events.ics">Veranstaltungskalender als ICS</a>
</div> </div>

@ -1,7 +1,9 @@
<div id="mc_embed_signup"> <div id="mc_embed_signup" class="newsletter-cta p-3 mb-4 rounded">
<p class="newsletter-heading mb-2">Bleiben Sie informiert!</p>
<p class="newsletter-text mb-3">Erhalten Sie Updates zu unseren Veranstaltungen und Angeboten.</p>
<a href="https://gmbh.us8.list-manage.com/subscribe?u=a2db855ea544f9ced14052793&id=42a1a3ba11" <a href="https://gmbh.us8.list-manage.com/subscribe?u=a2db855ea544f9ced14052793&id=42a1a3ba11"
target="_blank"> target="_blank" class="btn btn-primary w-100 d-flex align-items-center justify-content-center">
<i class="fa fa-envelope"></i> <i class="bi bi-envelope-fill me-2"></i>
Zum Newsletter anmelden Zum Newsletter anmelden
</a> </a>
</div> </div>

@ -22,6 +22,22 @@ def footer_newsletter(context):
return {'request': context['request']} return {'request': context['request']}
@register.simple_tag
def get_upcoming_events(limit=3):
"""
Returns a specified number of upcoming events ordered by start date.
External events are excluded from the results.
Usage: {% get_upcoming_events 3 as upcoming_events %}
"""
from core.models import EventPage
return EventPage.objects.live().filter(
show_in_event_calendar=True,
is_external=False,
).filter(
Q(end_date__gte=datetime.datetime.now()) | Q(end_date__isnull=True, start_date__gte=datetime.datetime.now())
).order_by('start_date')[:limit]
@register.inclusion_tag('core/tags/event_calendar.html', takes_context=True) @register.inclusion_tag('core/tags/event_calendar.html', takes_context=True)
def event_calendar(context): def event_calendar(context):
from core.models import EventPage from core.models import EventPage

@ -40,6 +40,31 @@ services:
PYTHONDONTWRITEBYTECODE: 1 PYTHONDONTWRITEBYTECODE: 1
DJANGO_SETTINGS_MODULE: feo_homepage.settings.prod DJANGO_SETTINGS_MODULE: feo_homepage.settings.prod
celery:
build:
context: .
args:
UID: "${UID}"
GID: "${GID}"
entrypoint:
- 'celery'
command: "-A feo_homepage worker --beat -l INFO"
environment:
PYTHONDONTWRITEBYTECODE: 1
DJANGO_SETTINGS_MODULE: feo_homepage.settings.prod
volumes:
- .:/app
- userhome:/home/uid1000
env_file:
- .env
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:alpine
restart: unless-stopped
volumes: volumes:
userhome: userhome:
driver: local driver: local

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

@ -0,0 +1,14 @@
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'feo_homepage.settings.base')
app = Celery('feo_homepage')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
import os import os
from pathlib import Path from pathlib import Path
from celery.schedules import crontab
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -174,3 +175,17 @@ CRISPY_TEMPLATE_PACK = 'bootstrap5'
RECAPTCHA_PUBLIC_KEY = '6Lfa2XAUAAAAAKaTdzFBc4zrN8YxrsW9kUpWjkom' RECAPTCHA_PUBLIC_KEY = '6Lfa2XAUAAAAAKaTdzFBc4zrN8YxrsW9kUpWjkom'
RECAPTCHA_PRIVATE_KEY = '6Lfa2XAUAAAAAGPHGmkceJlPXm3iwXvlDQbXBC0s' RECAPTCHA_PRIVATE_KEY = '6Lfa2XAUAAAAAGPHGmkceJlPXm3iwXvlDQbXBC0s'
NOCAPTCHA = True NOCAPTCHA = True
# Celery settings
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://redis:6379/0')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULE = {
'move_past_events_to_history': {
'task': 'core.tasks.move_past_events_to_history',
'schedule': crontab(hour=3, minute=0), # Run daily at 3:00 AM
},
}

@ -9,3 +9,5 @@ django-crispy-forms
crispy-bootstrap5 crispy-bootstrap5
django-braces django-braces
daphne daphne
celery
redis

@ -4,6 +4,8 @@
# #
# pip-compile # pip-compile
# #
amqp==5.3.1
# via kombu
anyascii==0.3.2 anyascii==0.3.2
# via wagtail # via wagtail
asgiref==3.8.1 asgiref==3.8.1
@ -20,12 +22,28 @@ automat==25.4.16
# via twisted # via twisted
beautifulsoup4==4.13.4 beautifulsoup4==4.13.4
# via wagtail # via wagtail
billiard==4.2.1
# via celery
celery==5.5.2
# via -r requirements.in
certifi==2025.4.26 certifi==2025.4.26
# via requests # via requests
cffi==1.17.1 cffi==1.17.1
# via cryptography # via cryptography
charset-normalizer==3.4.1 charset-normalizer==3.4.1
# via requests # via requests
click==8.1.8
# via
# celery
# click-didyoumean
# click-plugins
# click-repl
click-didyoumean==0.3.1
# via celery
click-plugins==1.1.1
# via celery
click-repl==0.3.0
# via celery
constantly==23.10.4 constantly==23.10.4
# via twisted # via twisted
crispy-bootstrap5==2025.4 crispy-bootstrap5==2025.4
@ -103,6 +121,8 @@ idna==3.10
# twisted # twisted
incremental==24.7.2 incremental==24.7.2
# via twisted # via twisted
kombu==5.5.3
# via celery
laces==0.1.2 laces==0.1.2
# via wagtail # via wagtail
openpyxl==3.1.5 openpyxl==3.1.5
@ -113,6 +133,8 @@ pillow==11.2.1
# wagtail # wagtail
pillow-heif==0.21.0 pillow-heif==0.21.0
# via willow # via willow
prompt-toolkit==3.0.51
# via click-repl
psycopg[binary]==3.2.6 psycopg[binary]==3.2.6
# via -r requirements.in # via -r requirements.in
psycopg-binary==3.2.6 psycopg-binary==3.2.6
@ -128,7 +150,11 @@ pycparser==2.22
pyopenssl==25.0.0 pyopenssl==25.0.0
# via twisted # via twisted
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
# via icalendar # via
# celery
# icalendar
redis==5.2.1
# via -r requirements.in
requests==2.32.3 requests==2.32.3
# via wagtail # via wagtail
service-identity==24.2.0 service-identity==24.2.0
@ -154,9 +180,16 @@ typing-extensions==4.13.2
# pyopenssl # pyopenssl
# twisted # twisted
tzdata==2025.2 tzdata==2025.2
# via icalendar # via
# icalendar
# kombu
urllib3==2.4.0 urllib3==2.4.0
# via requests # via requests
vine==5.1.0
# via
# amqp
# celery
# kombu
wagtail==6.4.1 wagtail==6.4.1
# via # via
# -r requirements.in # -r requirements.in
@ -165,6 +198,8 @@ wagtail-django-recaptcha==2.1.1
# via -r requirements.in # via -r requirements.in
wagtailmedia==0.15.2 wagtailmedia==0.15.2
# via -r requirements.in # via -r requirements.in
wcwidth==0.2.13
# via prompt-toolkit
willow[heif]==1.10.0 willow[heif]==1.10.0
# via # via
# wagtail # wagtail

Loading…
Cancel
Save