Compare commits

...
3 Commits
Author SHA1 Message Date
ponas 93956435cf improve full menu animation 2026-07-05 13:02:22 +02:00
ponas 39364b5023 avoid layout shifts on event index page 2026-07-05 12:53:53 +02:00
ponas b46320ea94 add a duplicate button to event occurrences 2026-07-05 12:12:48 +02:00
5 changed files with 92 additions and 14 deletions
+70
View File
@@ -16,6 +16,76 @@ function registerPricingPanelToggler() {
} }
} }
// Adds a "Duplicate" button to each event occurrence ("Forekomst") row, copying
// its date, time and venue. Wagtail's InlinePanel has no native duplicate.
function registerOccurrenceCloning() {
const FORMSET_PREFIX = "occurrences";
const container = document.getElementById(`id_${FORMSET_PREFIX}-FORMS`);
if (!container) return; // not an event page
const FIELDS = ["start", "end", "venue", "venue_custom"];
const duplicateIcon =
'<svg class="icon icon-copy" aria-hidden="true"><use href="#icon-copy"></use></svg>';
function duplicateRow(sourceChild) {
const addButton = document.getElementById(`id_${FORMSET_PREFIX}-ADD`);
const totalForms = document.getElementById(
`id_${FORMSET_PREFIX}-TOTAL_FORMS`
);
if (!addButton || !totalForms) return;
// `inline_child_occurrences-0` -> form prefix `occurrences-0`, used by field ids.
const sourcePrefix = sourceChild.id.replace(/^inline_child_/, "");
const values = {};
FIELDS.forEach((name) => {
const field = document.getElementById(`id_${sourcePrefix}-${name}`);
values[name] = field ? field.value : "";
});
// The new form takes the current TOTAL_FORMS index. Click Wagtail's real
// "Add" so it appends and initialises the row (e.g. date/time pickers).
const newIndex = parseInt(totalForms.value, 10);
addButton.click();
const newPrefix = `${FORMSET_PREFIX}-${newIndex}`;
FIELDS.forEach((name) => {
const field = document.getElementById(`id_${newPrefix}-${name}`);
if (!field) return;
field.value = values[name];
field.dispatchEvent(new Event("input", { bubbles: true }));
field.dispatchEvent(new Event("change", { bubbles: true }));
});
}
function addDuplicateButton(child) {
const controls = child.querySelector("[data-panel-controls]");
if (!controls || controls.querySelector("[data-duplicate-occurrence]")) return;
const button = document.createElement("button");
button.type = "button";
button.className = "button button--icon text-replace white";
button.title = "Duplicate";
button.setAttribute("data-duplicate-occurrence", "");
button.innerHTML = duplicateIcon;
button.addEventListener("click", () => duplicateRow(child));
// Sit just before the delete button, matching the native control order.
const deleteButton = controls.querySelector('[id$="-DELETE-button"]');
controls.insertBefore(button, deleteButton);
}
function refresh() {
container
.querySelectorAll("[data-inline-panel-child]")
.forEach((child) => addDuplicateButton(child));
}
// Cover existing rows plus any added later (via "Add" or "Duplicate").
refresh();
new MutationObserver(() => refresh()).observe(container, { childList: true });
}
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
registerPricingPanelToggler(); registerPricingPanelToggler();
registerOccurrenceCloning();
}); });
+6 -1
View File
@@ -18,7 +18,12 @@ export async function generateMetadata(
return getSeoMetadata(data.index as EventIndexFragment, parent); return getSeoMetadata(data.index as EventIndexFragment, parent);
} }
export default async function Page() { export default async function Page({
searchParams,
}: {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
}) {
await searchParams;
const props = await loadEventIndexProps(); const props = await loadEventIndexProps();
return <EventIndexView {...props} />; return <EventIndexView {...props} />;
} }
+6 -9
View File
@@ -1,4 +1,3 @@
import { Suspense } from "react";
import { VenueFragment } from "@/gql/graphql"; import { VenueFragment } from "@/gql/graphql";
import { getClient } from "@/app/client"; import { getClient } from "@/app/client";
import { EventContainer } from "@/components/events/EventContainer"; import { EventContainer } from "@/components/events/EventContainer";
@@ -46,14 +45,12 @@ export function EventIndexView({
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
<PageHeader heading="Dette skjer på Chateau Neuf" align="left" /> <PageHeader heading="Dette skjer på Chateau Neuf" align="left" />
<Suspense> <EventContainer
<EventContainer events={events}
events={events} eventCategories={eventCategories}
eventCategories={eventCategories} eventOrganizers={eventOrganizers}
eventOrganizers={eventOrganizers} venues={venues}
venues={venues} />
/>
</Suspense>
</main> </main>
); );
} }
@@ -1,3 +1,8 @@
.events {
// keep the footer below the fold while the event list renders
min-height: 100vh;
}
.eventWrapper { .eventWrapper {
//margin: calc(var(--spacing-sitepadding-block)*-1) calc(var(--spacing-sitepadding-inline)*-1); //margin: calc(var(--spacing-sitepadding-block)*-1) calc(var(--spacing-sitepadding-inline)*-1);
} }
+4 -3
View File
@@ -18,7 +18,7 @@
} }
.fullMenu { .fullMenu {
right: 0; transform: translateX(0);
} }
.toggleMenu { .toggleMenu {
@@ -137,18 +137,19 @@
.fullMenu { .fullMenu {
position: fixed; position: fixed;
right: -100%; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
z-index: 1000; z-index: 1000;
width: 50%; width: 50%;
overflow-y: auto; overflow-y: auto;
transform: translateX(100%);
background: var(--color-menu-background); background: var(--color-menu-background);
color: var(--color-menu-text); color: var(--color-menu-text);
padding: var(--spacing-sitepadding-block); padding: var(--spacing-sitepadding-block);
transition: all var(--transition-easing); transition: transform var(--transition-easing);
a { a {
--size-icon: var(--font-size-caption); --size-icon: var(--font-size-caption);