import { localized, msg } from "@lit/localize"; import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"; import SlProgressRing from "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring.component.js"; import { LitElementWw } from "@webwriter/lit"; import { css, PropertyValues } from "lit"; import { html, nothing } from "lit-html"; import { property, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import LOCALIZE from "../../localization/generated"; import { TimelineDate } from "../util/timeline-date"; import { TimelineTemplate } from "../util/timeline-template.component"; export type QuizEvent = { id: string; titleHtml: string; date: TimelineDate; endDate: TimelineDate | null; }; @localized() export class QuizContainer extends LitElementWw { protected localize = LOCALIZE; // We cannot use a custom application/x- MIME type here because // mobile browsers do not support them in drag-and-drop operations. private static DRAG_DATA_TYPE = "text/plain"; /** @internal */ static scopedElements = { "timeline-template": TimelineTemplate, "sl-button": SlButton, "sl-progress-ring": SlProgressRing, }; static styles = css` :host { width: 100%; display: grid; grid-template-columns: calc(50% - 1em) auto; gap: 0 0.5em; align-items: start; } .empty-quiz { grid-column: 1 / -1; padding: var(--sl-spacing-x-small); padding-left: 1.5rem; color: var(--sl-color-neutral-500); } .unassigned-events-container { display: flex; flex-direction: column; padding: var(--sl-spacing-small) 0; gap: var(--sl-spacing-small); height: 100%; box-sizing: border-box; } .results-container { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sl-spacing-medium); padding-top: var(--sl-spacing-x-large); text-align: center; } .card-base { width: 100%; border-radius: var(--sl-border-radius-medium); padding: var(--sl-spacing-x-small) var(--sl-spacing-small); box-sizing: border-box; } .card-event { background-color: var(--sl-color-neutral-0); border: var(--sl-color-neutral-200) solid 1px; transition: var(--sl-transition-fast) border-color, var(--sl-transition-fast) background-color; &.card-correct { border-color: var(--sl-color-success-500); background-color: var(--sl-color-success-50); } &.card-incorrect { border-color: var(--sl-color-danger-500); background-color: var(--sl-color-danger-50); } &[draggable="true"] { cursor: grab; } &[draggable="true"]:hover { border-color: var(--sl-color-primary-300); background-color: var(--sl-color-primary-50); } } .card-placeholder { background-color: var(--sl-color-neutral-100); height: calc(1.5em + (var(--sl-spacing-x-small) + 1px) * 2); transition: var(--sl-transition-fast) background-color; .drag-over & { background-color: var(--sl-color-neutral-200); } } .dot { width: 100%; aspect-ratio: 1 / 1; } .dot::before { content: ""; display: block; margin: 0 auto; margin-top: 9px; height: 0.5em; aspect-ratio: 1 / 1; border-radius: 50%; background-color: black; outline: 4px solid white; /* Ensures that the dot is above the timeline line */ position: relative; z-index: 1; } .help-text { font-size: var(--sl-font-size-small); color: var(--sl-color-neutral-500); } `; @property({ type: Array, attribute: true }) accessor events: QuizEvent[] = []; @state() accessor assignments: { id: string; assignedToId: string | null }[] = []; @state() accessor checkAnswers: boolean = false; protected update(changedProperties: PropertyValues): void { if (changedProperties.has("events")) { // Remove assignments for events that no longer exist const eventIds = new Set(this.events.map((e) => e.id)); this.assignments = this.assignments.filter((a) => eventIds.has(a.id)); // Add assignments for new events, in random order for (const event of this.events) { if (!this.assignments.find((a) => a.id === event.id)) { const randomIndex = Math.floor(Math.random() * this.assignments.length); this.assignments.splice(randomIndex, 0, { id: event.id, assignedToId: null }); } } } super.update(changedProperties); } private resetAssignments() { for (const assignment of this.assignments) { assignment.assignedToId = null; } this.checkAnswers = false; this.requestUpdate(); } private extractEventIdFromDragEvent(e: DragEvent): string | null { return e.dataTransfer?.getData(QuizContainer.DRAG_DATA_TYPE) ?? null; } private EventCard(event: QuizEvent, correct?: boolean) { let cardClasses = "card-base card-event"; if (this.checkAnswers && correct !== undefined) { if (correct) cardClasses += " card-correct"; else cardClasses += " card-incorrect"; } return html`