v2 with admin changes

This commit is contained in:
Flatlogic Bot 2026-04-02 16:08:07 +00:00
parent 693107e079
commit 25c22e7107
9 changed files with 226 additions and 2 deletions

View File

@ -5,7 +5,8 @@
{% block content %}
<section class="embed-section">
<div class="container-fluid px-md-4 px-lg-5">
<div class="embed-shell-card">
<div class="embed-shell-card {% if not show_embed_header %}embed-shell-card-compact{% endif %}">
{% if show_embed_header %}
<div class="embed-header d-flex flex-column flex-md-row justify-content-between gap-3 align-items-md-center">
<div>
<span class="eyebrow-pill">Embeddable widget</span>
@ -14,6 +15,7 @@
</div>
<a class="btn btn-ghost" href="{% url 'calendar_page' %}" target="_blank" rel="noopener">Open full page</a>
</div>
{% endif %}
{% include "core/includes/calendar_widget.html" with calendar_variant="embed" %}
</div>
</div>

View File

@ -17,6 +17,75 @@
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-xl-5">
<div class="card-surface embed-settings-card h-100">
<span class="eyebrow-pill">Embed settings</span>
<h2 class="h4 mt-3 mb-2">Generate a copy-ready widget snippet</h2>
<p class="section-copy">Tune the iframe once, copy the finished code, and paste it into your existing HTML site.</p>
<form class="embed-settings-form" data-embed-settings data-embed-base-url="{{ embed_base_url }}">
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label" for="embedTitle">Iframe title</label>
<input class="form-control" id="embedTitle" name="title" type="text" value="Where to find us calendar">
</div>
<div class="col-sm-6">
<label class="form-label" for="embedMonth">Starting month</label>
<input class="form-control" id="embedMonth" name="month" type="month" value="{{ default_embed_month }}">
</div>
<div class="col-sm-6">
<label class="form-label" for="embedWidth">Width</label>
<input class="form-control" id="embedWidth" name="width" type="text" value="100%">
</div>
<div class="col-sm-6">
<label class="form-label" for="embedHeight">Height (px)</label>
<input class="form-control" id="embedHeight" name="height" type="number" min="360" step="10" value="760">
</div>
<div class="col-sm-6">
<label class="form-label" for="embedRadius">Corner radius (px)</label>
<input class="form-control" id="embedRadius" name="radius" type="number" min="0" step="2" value="24">
</div>
<div class="col-sm-6">
<label class="form-label" for="embedHeader">Widget header</label>
<select class="form-select" id="embedHeader" name="header">
<option value="1" selected>Show header and action</option>
<option value="0">Hide header for a tighter embed</option>
</select>
</div>
</div>
</form>
</div>
</div>
<div class="col-xl-7">
<div class="card-surface code-card h-100">
<div class="code-card-header d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center">
<div>
<strong>Live embed output</strong>
<p class="mb-0 text-white-50 small">Copy either the direct widget URL or the full iframe snippet.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-sm btn-primary-brand" href="{{ embed_base_url }}" target="_blank" rel="noopener" data-embed-preview>Preview widget</a>
<button class="btn btn-sm btn-ghost" type="button" data-copy-snippet data-copy-target="#dashboardEmbedUrl">Copy URL</button>
<button class="btn btn-sm btn-ghost" type="button" data-copy-snippet data-copy-target="#dashboardIframeSnippet">Copy iframe</button>
</div>
</div>
<div class="embed-output-stack mt-4">
<div>
<small class="text-uppercase text-white-50 d-block mb-2">Widget URL</small>
<pre id="dashboardEmbedUrl" class="code-snippet mb-0">{{ embed_base_url }}</pre>
</div>
<div>
<small class="text-uppercase text-white-50 d-block mb-2">Iframe snippet</small>
<pre id="dashboardIframeSnippet" class="code-snippet mb-0">&lt;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"&gt;&lt;/iframe&gt;</pre>
</div>
</div>
</div>
</div>
</div>
<div class="table-card card-surface">
<div class="d-flex flex-column flex-md-row justify-content-between gap-3 align-items-md-center mb-3">
<div>

View File

@ -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):

View File

@ -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'<iframe src="{embed_url}" title="Where to find us calendar" width="100%" height="760" style="border:0;border-radius:24px;overflow:hidden;"></iframe>'
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,
),
)

View File

@ -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);

View File

@ -103,4 +103,51 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
const escapeAttribute = (value) => String(value || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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 = `<iframe src="${escapeAttribute(widgetUrl)}" title="${escapeAttribute(title)}" width="${escapeAttribute(width)}" height="${height}" style="border:0;border-radius:${radius}px;overflow:hidden;" loading="lazy"></iframe>`;
urlOutput.textContent = widgetUrl;
iframeOutput.textContent = iframeSnippet;
if (previewLink) {
previewLink.href = widgetUrl;
}
};
['input', 'change'].forEach((eventName) => {
form.addEventListener(eventName, updateEmbedOutput);
});
updateEmbedOutput();
});
});

View File

@ -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);

View File

@ -103,4 +103,51 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
const escapeAttribute = (value) => String(value || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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 = `<iframe src="${escapeAttribute(widgetUrl)}" title="${escapeAttribute(title)}" width="${escapeAttribute(width)}" height="${height}" style="border:0;border-radius:${radius}px;overflow:hidden;" loading="lazy"></iframe>`;
urlOutput.textContent = widgetUrl;
iframeOutput.textContent = iframeSnippet;
if (previewLink) {
previewLink.href = widgetUrl;
}
};
['input', 'change'].forEach((eventName) => {
form.addEventListener(eventName, updateEmbedOutput);
});
updateEmbedOutput();
});
});