class MatchesWidget extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.isInitialized = false; this.resizeObserver = null; this.state = { currentMatchDate: new Date(), visiblePickerMonth: new Date(), isDatePickerOpen: false, matches: [], loading: false, error: null, }; this.state.currentMatchDate.setHours(0, 0, 0, 0); this.state.visiblePickerMonth = new Date(this.state.currentMatchDate.getFullYear(), this.state.currentMatchDate.getMonth(), 1); this.handleDocumentClick = this.handleDocumentClick.bind(this); this.handleDocumentKeydown = this.handleDocumentKeydown.bind(this); } connectedCallback() { if (this.isInitialized) { return; } this.isInitialized = true; this.renderShell(); this.appContainerElement = this.shadowRoot.querySelector(".app-container"); this.matchDateElement = this.shadowRoot.getElementById("match-date"); this.datePickerElement = this.shadowRoot.getElementById("match-date-picker"); this.datePickerAnchorElement = this.shadowRoot.querySelector(".matches-date-picker-anchor"); this.matchesContentElement = this.shadowRoot.getElementById("matches-content"); this.previousDateButton = this.shadowRoot.querySelector('[data-direction="previous"]'); this.nextDateButton = this.shadowRoot.querySelector('[data-direction="next"]'); if ( !this.appContainerElement || !this.matchDateElement || !this.datePickerElement || !this.datePickerAnchorElement || !this.matchesContentElement ) { return; } const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; this.dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric", timeZone: browserTimeZone, }); this.timeFormatter = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", timeZone: browserTimeZone, }); this.monthFormatter = new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric", timeZone: browserTimeZone, }); this.weekdayFormatter = new Intl.DateTimeFormat(undefined, { weekday: "short", timeZone: browserTimeZone, }); this.matchDateElement.addEventListener("click", () => { this.toggleDatePicker(); }); this.datePickerElement.addEventListener("click", (event) => { this.handleDatePickerClick(event); }); document.addEventListener("click", this.handleDocumentClick); document.addEventListener("keydown", this.handleDocumentKeydown); this.matchesContentElement.addEventListener("click", (event) => { if (event.target.closest(".match-broadcast-link")) { return; } const matchCard = event.target.closest(".match-card"); if (!matchCard) { return; } this.toggleMatchCard(matchCard); }); this.matchesContentElement.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") { return; } if (event.target.closest(".match-broadcast-link")) { return; } const matchCard = event.target.closest(".match-card"); if (!matchCard) { return; } event.preventDefault(); this.toggleMatchCard(matchCard); }); this.previousDateButton?.addEventListener("click", () => { const previousDate = new Date(this.state.currentMatchDate); previousDate.setDate(previousDate.getDate() - 1); this.setCurrentMatchDate(previousDate); this.fetchUpcomingMatches(); }); this.nextDateButton?.addEventListener("click", () => { const nextDate = new Date(this.state.currentMatchDate); nextDate.setDate(nextDate.getDate() + 1); this.setCurrentMatchDate(nextDate); this.fetchUpcomingMatches(); }); if (typeof ResizeObserver !== "undefined") { this.resizeObserver = new ResizeObserver(() => { this.updateExpandedHeights(); }); this.resizeObserver.observe(this.appContainerElement); } this.renderMatchDate(); this.renderDatePicker(); this.renderMatches(); this.fetchUpcomingMatches(); } disconnectedCallback() { this.resizeObserver?.disconnect(); this.resizeObserver = null; document.removeEventListener("click", this.handleDocumentClick); document.removeEventListener("keydown", this.handleDocumentKeydown); } escapeHtml(value = "") { return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } formatApiDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}T00:00:00Z`; } getSelectedDayRequestRange() { const startDate = new Date(this.state.currentMatchDate); startDate.setHours(0, 0, 0, 0); const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 1); return { startDate, endDate }; } normalizeHexColor(color) { if (!color || typeof color !== "string") { return null; } return color.replace(/^0x[0-9a-fA-F]{2}/, "#"); } normalizeBroadcastLink(value) { if (typeof value !== "string") { return null; } const trimmedValue = value.trim(); if (!trimmedValue) { return null; } try { const parsedUrl = new URL(trimmedValue); if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { return null; } return parsedUrl.toString(); } catch { return null; } } getTeamColorStyle(team) { const primaryColor = team?.primary_color_hex || "#d9d9d9"; const secondaryColor = team?.secondary_color_hex || primaryColor; return `background: linear-gradient(to right, ${primaryColor} 50%, ${secondaryColor} 50%);`; } normalizeMatch(match) { console.log("Normalizing match:", match); return { id: match._id, //venueName: match.pitch?.name || match.club_name || "Unknown venue", tournamentName: match.tournament_name || "Unknown tournament", venueName: match.club_name || "Unknown venue", clubLogo: match.club_logo, broadcastLink: this.normalizeBroadcastLink(match.broadcast_link), startTimeLabel: match.start_time ? this.timeFormatter.format(new Date(match.start_time)) : "TBD", scoreOne: match.score_one, scoreTwo: match.score_two, teamOne: { name: match.team_one?.name || "Team One", primary_color_hex: this.normalizeHexColor(match.team_one?.primary_color), secondary_color_hex: this.normalizeHexColor(match.team_one?.secondary_color), players: match.team_one?.players || [], }, teamTwo: { name: match.team_two?.name || "Team Two", primary_color_hex: this.normalizeHexColor(match.team_two?.primary_color), secondary_color_hex: this.normalizeHexColor(match.team_two?.secondary_color), players: match.team_two?.players || [], }, }; } renderPlayerList(players = []) { if (!players.length) { return `
  • No lineup available -
  • `; } return players .map( (player) => `
  • ${this.escapeHtml(player.name || "Unknown player")} ${this.escapeHtml(player.handicap ?? "-")}
  • `, ) .join(""); } calculateTeamTotal(players = []) { return players.reduce((total, player) => total + (Number(player?.handicap) || 0), 0); } renderMatchTeamTotal(players = []) { if (!players.length) { return ""; } const total = this.calculateTeamTotal(players); return `
    Total ${this.escapeHtml(total)}
    `; } renderMatchTeamSection(team) { return `
    ${this.escapeHtml(team.name)}
    ${this.renderMatchTeamTotal(team.players)}
    `; } renderBroadcastAction(match, showBroadcastColumn) { if (!showBroadcastColumn) { return ""; } if (!match.broadcastLink) { return ''; } return `
    Live Stream
    `; } renderBroadcastBalanceSlot(showBroadcastColumn) { if (!showBroadcastColumn) { return ""; } return ''; } renderMatchCard(match, showBroadcastColumn = false) { const matchCardClassName = showBroadcastColumn ? "match-card match-card--with-broadcast-column" : "match-card"; const scoreSummaryClassName = showBroadcastColumn ? "score-summary score-summary--with-broadcast" : "score-summary"; return ` `; } renderMatchDate() { this.matchDateElement.textContent = this.dateFormatter.format(this.state.currentMatchDate); this.matchDateElement.setAttribute("aria-expanded", String(this.state.isDatePickerOpen)); } setCurrentMatchDate(date) { const nextDate = new Date(date); nextDate.setHours(0, 0, 0, 0); this.state.currentMatchDate = nextDate; this.state.visiblePickerMonth = new Date(nextDate.getFullYear(), nextDate.getMonth(), 1); this.renderMatchDate(); this.renderDatePicker(); } toggleDatePicker() { if (this.state.isDatePickerOpen) { this.closeDatePicker(); return; } this.openDatePicker(); } openDatePicker() { this.state.visiblePickerMonth = new Date(this.state.currentMatchDate.getFullYear(), this.state.currentMatchDate.getMonth(), 1); this.state.isDatePickerOpen = true; this.renderDatePicker(); } closeDatePicker() { if (!this.state.isDatePickerOpen) { return; } this.state.isDatePickerOpen = false; this.renderDatePicker(); } handleDocumentClick(event) { if (!this.state.isDatePickerOpen) { return; } if (event.composedPath().includes(this.datePickerAnchorElement)) { return; } this.closeDatePicker(); } handleDocumentKeydown(event) { if (event.key !== "Escape" || !this.state.isDatePickerOpen) { return; } this.closeDatePicker(); this.matchDateElement?.focus(); } handleDatePickerClick(event) { const actionButton = event.target.closest("[data-date-action]"); if (actionButton?.dataset.dateAction === "today") { const today = new Date(); today.setHours(0, 0, 0, 0); this.setCurrentMatchDate(today); this.closeDatePicker(); this.fetchUpcomingMatches(); return; } const monthButton = event.target.closest("[data-calendar-direction]"); if (monthButton) { const monthOffset = monthButton.dataset.calendarDirection === "previous" ? -1 : 1; this.state.visiblePickerMonth = new Date( this.state.visiblePickerMonth.getFullYear(), this.state.visiblePickerMonth.getMonth() + monthOffset, 1, ); this.renderDatePicker(); return; } const dateButton = event.target.closest("[data-date-value]"); if (!dateButton) { return; } const selectedDate = this.parseDateValue(dateButton.dataset.dateValue); if (!selectedDate) { return; } this.setCurrentMatchDate(selectedDate); this.closeDatePicker(); this.fetchUpcomingMatches(); } parseDateValue(dateValue) { if (!dateValue) { return null; } const [year, month, day] = dateValue.split("-").map((value) => Number.parseInt(value, 10)); if (!year || !month || !day) { return null; } return new Date(year, month - 1, day); } formatDateValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } isSameDate(firstDate, secondDate) { return ( firstDate.getFullYear() === secondDate.getFullYear() && firstDate.getMonth() === secondDate.getMonth() && firstDate.getDate() === secondDate.getDate() ); } getWeekdayLabels() { const sunday = new Date(2024, 0, 7); return Array.from({ length: 7 }, (_, index) => { const weekdayDate = new Date(sunday); weekdayDate.setDate(sunday.getDate() + index); return this.weekdayFormatter.format(weekdayDate); }); } getCalendarDays() { const year = this.state.visiblePickerMonth.getFullYear(); const month = this.state.visiblePickerMonth.getMonth(); const firstDayOfMonth = new Date(year, month, 1); const daysInMonth = new Date(year, month + 1, 0).getDate(); const leadingEmptyDays = firstDayOfMonth.getDay(); const calendarDays = []; for (let index = 0; index < leadingEmptyDays; index += 1) { calendarDays.push(null); } for (let day = 1; day <= daysInMonth; day += 1) { calendarDays.push(new Date(year, month, day)); } while (calendarDays.length % 7 !== 0) { calendarDays.push(null); } return calendarDays; } renderDatePickerDay(date) { if (!date) { return ''; } const today = new Date(); today.setHours(0, 0, 0, 0); const isSelected = this.isSameDate(date, this.state.currentMatchDate); const isToday = this.isSameDate(date, today); const classNames = ["date-picker-day-button"]; if (isSelected) { classNames.push("is-selected"); } if (isToday) { classNames.push("is-today"); } return ` `; } renderDatePicker() { if (!this.datePickerElement) { return; } this.matchDateElement?.setAttribute("aria-expanded", String(this.state.isDatePickerOpen)); if (!this.state.isDatePickerOpen) { this.datePickerElement.hidden = true; this.datePickerElement.innerHTML = ""; return; } const weekdayLabels = this.getWeekdayLabels(); const calendarDays = this.getCalendarDays(); this.datePickerElement.hidden = false; this.datePickerElement.innerHTML = `
    ${this.escapeHtml(this.monthFormatter.format(this.state.visiblePickerMonth))}
    ${weekdayLabels.map((label) => `${this.escapeHtml(label)}`).join("")}
    ${calendarDays.map((date) => this.renderDatePickerDay(date)).join("")}
    `; } renderMatches() { if (this.state.loading) { this.matchesContentElement.innerHTML = '

    Loading matches...

    '; return; } if (this.state.error) { this.matchesContentElement.innerHTML = `

    ${this.escapeHtml(this.state.error)}

    `; return; } if (!this.state.matches.length) { this.matchesContentElement.innerHTML = '

    No matches scheduled for this date.

    '; return; } const showBroadcastColumn = this.state.matches.some((match) => Boolean(match.broadcastLink)); this.matchesContentElement.innerHTML = this.state.matches.map((match) => this.renderMatchCard(match, showBroadcastColumn)).join(""); } isMatchOnSelectedDay(match) { if (!match?.start_time) { return false; } const matchStartDate = new Date(match.start_time); if (Number.isNaN(matchStartDate.getTime())) { return false; } return this.isSameDate(matchStartDate, this.state.currentMatchDate); } async fetchUpcomingMatches() { const { startDate, endDate } = this.getSelectedDayRequestRange(); const requestUrl = new URL("https://api.lineuppolo.com/api/v2/widgetUpcomingMatches"); requestUrl.searchParams.set("start_date", this.formatApiDate(startDate)); requestUrl.searchParams.set("end_date", this.formatApiDate(endDate)); this.state.loading = true; this.state.error = null; this.renderMatches(); try { const response = await fetch(requestUrl); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } const data = await response.json(); console.log("Upcoming matches response:", data); const matches = data?.result?.result || []; this.state.matches = matches.filter((match) => this.isMatchOnSelectedDay(match)).map((match) => this.normalizeMatch(match)); } catch (error) { console.error("Failed to fetch upcoming matches:", error); this.state.matches = []; this.state.error = "Unable to load matches right now."; } finally { this.state.loading = false; this.renderMatches(); } } toggleMatchCard(matchCard) { const teamsContainer = matchCard?.querySelector(".teams-container"); if (!teamsContainer) { return; } const isExpanded = !teamsContainer.classList.contains("is-expanded"); if (isExpanded) { teamsContainer.classList.add("is-expanded"); teamsContainer.style.maxHeight = `${teamsContainer.scrollHeight}px`; } else { teamsContainer.style.maxHeight = "0px"; teamsContainer.classList.remove("is-expanded"); } matchCard.setAttribute("aria-expanded", String(isExpanded)); } updateExpandedHeights() { const expandedContainers = this.matchesContentElement.querySelectorAll(".teams-container.is-expanded"); expandedContainers.forEach((teamsContainer) => { teamsContainer.style.maxHeight = `${teamsContainer.scrollHeight}px`; }); } renderShell() { this.shadowRoot.innerHTML = `

    Matches

    `; } } customElements.define("matches-widget", MatchesWidget);