diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 148300c..f3bcfc6 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/calendar_embed.html b/core/templates/core/calendar_embed.html index c177e1b..9ec28e3 100644 --- a/core/templates/core/calendar_embed.html +++ b/core/templates/core/calendar_embed.html @@ -5,7 +5,8 @@ {% block content %}
-
+
+ {% if show_embed_header %}
Embeddable widget @@ -14,6 +15,7 @@
Open full page
+ {% endif %} {% include "core/includes/calendar_widget.html" with calendar_variant="embed" %}
diff --git a/core/templates/core/event_dashboard.html b/core/templates/core/event_dashboard.html index 7eadcb2..a88cf28 100644 --- a/core/templates/core/event_dashboard.html +++ b/core/templates/core/event_dashboard.html @@ -17,6 +17,75 @@
+
+
+
+ Embed settings +

Generate a copy-ready widget snippet

+

Tune the iframe once, copy the finished code, and paste it into your existing HTML site.

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
+ Live embed output +

Copy either the direct widget URL or the full iframe snippet.

+
+
+ Preview widget + + +
+
+ +
+
+ Widget URL +
{{ embed_base_url }}
+
+
+ Iframe snippet +
<iframe src="{{ embed_base_url }}" title="Where to find us calendar" width="100%" height="760" style="border:0;border-radius:24px;overflow:hidden;" loading="lazy"></iframe>
+
+
+
+
+
+
diff --git a/core/tests.py b/core/tests.py index 20438a9..60b2f28 100644 --- a/core/tests.py +++ b/core/tests.py @@ -45,6 +45,12 @@ class CalendarViewTests(TestCase): response = client.get(reverse('event_dashboard')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Manage your public event calendar securely') + self.assertContains(response, 'Generate a copy-ready widget snippet') + + def test_embed_page_can_hide_header(self): + response = self.client.get(reverse('calendar_embed') + '?header=0') + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'Open full page') class EventDashboardMutationTests(TestCase): diff --git a/core/views.py b/core/views.py index a5bd905..47f241f 100644 --- a/core/views.py +++ b/core/views.py @@ -107,6 +107,14 @@ def _base_context(**extra): return context +def _embed_base_url(request): + return request.build_absolute_uri(reverse('calendar_embed')) + + +def _show_embed_header(request): + return request.GET.get('header', '1') != '0' + + @login_required(login_url='login') def event_dashboard(request): if not request.user.is_staff: @@ -119,6 +127,8 @@ def event_dashboard(request): page_title='Manage events', events=events, dashboard_count=events.count(), + embed_base_url=_embed_base_url(request), + default_embed_month=timezone.localdate().replace(day=1).strftime('%Y-%m'), ), ) @@ -221,7 +231,7 @@ def event_dashboard_detail(request, slug): def home(request): month_context = _build_month_context(request.GET.get('month')) upcoming_events = list(Event.objects.filter(is_published=True, end__gte=timezone.now()).order_by('start', 'name')[:6]) - embed_url = request.build_absolute_uri(reverse('calendar_embed')) + embed_url = _embed_base_url(request) iframe_snippet = f'' return render( request, @@ -257,6 +267,7 @@ def calendar_embed(request): _base_context( page_title='Embeddable calendar', embedded=True, + show_embed_header=_show_embed_header(request), **month_context, ), ) diff --git a/static/css/custom.css b/static/css/custom.css index 75d37a1..a41b0a1 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -670,6 +670,24 @@ span { color: #fff2df; } +.embed-settings-card { + padding: 1.5rem; +} + +.embed-settings-form .form-label { + font-weight: 700; + color: var(--brand-ink); +} + +.embed-output-stack { + display: grid; + gap: 1rem; +} + +.embed-shell-card-compact { + padding-top: 0.8rem; +} + .dashboard-banner { display: flex; justify-content: space-between; @@ -693,9 +711,12 @@ span { input[type="text"], input[type="password"], input[type="url"], +input[type="month"], +input[type="number"], input[type="datetime-local"], textarea, .form-control, +.form-select, .form-check-input { border-radius: 16px; border-color: rgba(18, 32, 35, 0.12); diff --git a/static/js/calendar.js b/static/js/calendar.js index 0d7c6c2..08f3444 100644 --- a/static/js/calendar.js +++ b/static/js/calendar.js @@ -103,4 +103,51 @@ document.addEventListener('DOMContentLoaded', () => { } }); }); + + const escapeAttribute = (value) => String(value || '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + + document.querySelectorAll('[data-embed-settings]').forEach((form) => { + const baseUrl = form.dataset.embedBaseUrl || ''; + const urlOutput = document.getElementById('dashboardEmbedUrl'); + const iframeOutput = document.getElementById('dashboardIframeSnippet'); + const previewLink = document.querySelector('[data-embed-preview]'); + if (!baseUrl || !urlOutput || !iframeOutput) return; + + const updateEmbedOutput = () => { + const formData = new FormData(form); + const params = new URLSearchParams(); + const month = String(formData.get('month') || '').trim(); + const header = String(formData.get('header') || '1'); + const title = String(formData.get('title') || 'Where to find us calendar').trim() || 'Where to find us calendar'; + const width = String(formData.get('width') || '100%').trim() || '100%'; + const height = Math.max(parseInt(String(formData.get('height') || '760'), 10) || 760, 360); + const radius = Math.max(parseInt(String(formData.get('radius') || '24'), 10) || 24, 0); + + if (/^\d{4}-\d{2}$/.test(month)) { + params.set('month', month); + } + if (header === '0') { + params.set('header', '0'); + } + + const widgetUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl; + const iframeSnippet = ``; + + urlOutput.textContent = widgetUrl; + iframeOutput.textContent = iframeSnippet; + if (previewLink) { + previewLink.href = widgetUrl; + } + }; + + ['input', 'change'].forEach((eventName) => { + form.addEventListener(eventName, updateEmbedOutput); + }); + updateEmbedOutput(); + }); + }); diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 75d37a1..a41b0a1 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -670,6 +670,24 @@ span { color: #fff2df; } +.embed-settings-card { + padding: 1.5rem; +} + +.embed-settings-form .form-label { + font-weight: 700; + color: var(--brand-ink); +} + +.embed-output-stack { + display: grid; + gap: 1rem; +} + +.embed-shell-card-compact { + padding-top: 0.8rem; +} + .dashboard-banner { display: flex; justify-content: space-between; @@ -693,9 +711,12 @@ span { input[type="text"], input[type="password"], input[type="url"], +input[type="month"], +input[type="number"], input[type="datetime-local"], textarea, .form-control, +.form-select, .form-check-input { border-radius: 16px; border-color: rgba(18, 32, 35, 0.12); diff --git a/staticfiles/js/calendar.js b/staticfiles/js/calendar.js index 0d7c6c2..08f3444 100644 --- a/staticfiles/js/calendar.js +++ b/staticfiles/js/calendar.js @@ -103,4 +103,51 @@ document.addEventListener('DOMContentLoaded', () => { } }); }); + + const escapeAttribute = (value) => String(value || '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + + document.querySelectorAll('[data-embed-settings]').forEach((form) => { + const baseUrl = form.dataset.embedBaseUrl || ''; + const urlOutput = document.getElementById('dashboardEmbedUrl'); + const iframeOutput = document.getElementById('dashboardIframeSnippet'); + const previewLink = document.querySelector('[data-embed-preview]'); + if (!baseUrl || !urlOutput || !iframeOutput) return; + + const updateEmbedOutput = () => { + const formData = new FormData(form); + const params = new URLSearchParams(); + const month = String(formData.get('month') || '').trim(); + const header = String(formData.get('header') || '1'); + const title = String(formData.get('title') || 'Where to find us calendar').trim() || 'Where to find us calendar'; + const width = String(formData.get('width') || '100%').trim() || '100%'; + const height = Math.max(parseInt(String(formData.get('height') || '760'), 10) || 760, 360); + const radius = Math.max(parseInt(String(formData.get('radius') || '24'), 10) || 24, 0); + + if (/^\d{4}-\d{2}$/.test(month)) { + params.set('month', month); + } + if (header === '0') { + params.set('header', '0'); + } + + const widgetUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl; + const iframeSnippet = ``; + + urlOutput.textContent = widgetUrl; + iframeOutput.textContent = iframeSnippet; + if (previewLink) { + previewLink.href = widgetUrl; + } + }; + + ['input', 'change'].forEach((eventName) => { + form.addEventListener(eventName, updateEmbedOutput); + }); + updateEmbedOutput(); + }); + });