diff --git a/assets/css/analytics.css b/assets/css/analytics.css deleted file mode 100644 index 6a028911..00000000 --- a/assets/css/analytics.css +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Mailchimp Analytics Page Styles - * - * @package Mailchimp - */ - -.mailchimp-sf-analytics-filters { - display: flex; - flex-wrap: wrap; - align-items: flex-end; - gap: 16px; - margin-bottom: 20px; -} - -.mailchimp-sf-analytics-filter-group { - display: flex; - flex-direction: column; - gap: 4px; -} - -.mailchimp-sf-analytics-filter-group > label { - font-size: 14px; - font-weight: 400; - color: #1d2327; -} - -.mailchimp-sf-analytics-filter-group select { - min-width: 180px; - height: 36px; - padding: 0 8px; - border: 1px solid #8c8f94; - border-radius: 4px; - font-size: 14px; - color: #1d2327; - background-color: #fff; -} - -.mailchimp-sf-analytics-filter-group select:focus { - border-color: var(--mailchimp-color-link, #017e89); - box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); - outline: none; -} - -/* Date picker trigger */ -.mailchimp-sf-date-picker { - position: relative; -} - -.mailchimp-sf-date-picker-trigger { - display: inline-flex; - align-items: center; - justify-content: space-between; - gap: 8px; - min-width: 220px; - height: 40px; - padding: 0 10px; - border: 1px solid #c3ced5; - color: var(--mailchimp-color-text); - border-radius: 6px; - font-size: 14px; - background-color: #fff; - cursor: pointer; - text-align: left; - transition: all 0.2s; -} - -.mailchimp-sf-date-picker-trigger:hover { - border-color: var(--mailchimp-color-link, #017e89); -} - -.mailchimp-sf-date-picker-trigger:focus { - border-color: var(--mailchimp-color-link, #017e89); - box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); - outline: none; -} - -.mailchimp-sf-date-picker.is-open .mailchimp-sf-date-picker-trigger { - border-color: var(--mailchimp-color-link, #017e89); - box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); - background-color: rgba(1, 126, 137, 0.08); -} - -.mailchimp-sf-date-picker-trigger .dashicons { - font-size: 18px; - width: 18px; - height: 18px; - line-height: 18px; - color: #50575e; -} - -.mailchimp-sf-date-picker-trigger .indicator-date-picker { - display: flex; - position: absolute; - top: 0; - bottom: 0; - right: 0; - pointer-events: none; - justify-content: center; - padding: 0 8px; - width: 24px; -} - -/* Date picker popover */ -.mailchimp-sf-date-picker-popover { - display: none; - position: absolute; - top: calc(100% + 6px); - left: 0; - z-index: 100; - min-width: 480px; - padding: 20px; - background: #fff; - border: 1px solid #dcdcde; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.mailchimp-sf-date-picker-popover.is-open { - display: block; -} - -.mailchimp-sf-date-picker-popover-row { - display: flex; - gap: 12px; - margin-bottom: 16px; -} - -.mailchimp-sf-date-picker-field { - display: flex; - flex-direction: column; - gap: 6px; - flex: 1; -} - -.mailchimp-sf-date-picker-field label { - font-size: 14px; - font-weight: 400; - color: #4A5565; -} - -.mailchimp-sf-date-picker-input-wrap { - position: relative; - width: 100%; -} - -.mailchimp-sf-date-picker-field-calendar { - align-items: center; - bottom: 0; - display: flex; - justify-content: center; - padding: 0 6px; - pointer-events: none; - position: absolute; - right: 0; - top: 0; -} - -.mailchimp-sf-date-picker-field-calendar svg { - display: block; - fill: #50575e; - height: 18px; - width: 18px; -} - -#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field select, -#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field input[type="text"] { - height: 36px; - font-size: 14px; - color: #1d2327; - background-color: #fff; - width: 100%; -} - -#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field .mailchimp-sf-date-picker-input-wrap input[type="text"] { - padding-right: 30px; -} - -#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field select:focus, -#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field input[type="text"]:focus { - border-color: var(--mailchimp-color-link, #017e89); - box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); - outline: none; -} - -/* Popover actions — .mailchimp-sf-button.btn-primary / .btn-secondary.btn-small; pill shape to match design */ -.mailchimp-sf-date-picker-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - - -/* Content area */ -.mailchimp-sf-analytics-content { - border: 1px solid #D3D0C8; - border-radius: 8px; - width: 100%; - overflow: auto; - padding: 24px; - background: #fff; - min-height: 200px; - margin-bottom: 20px; - box-sizing: border-box; -} - -.mailchimp-sf-analytics-placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 200px; - color: #8c8f94; - font-size: 15px; -} - -/* Deep link */ -.mailchimp-sf-analytics-deep-link { - margin-top: 16px; -} - -.mailchimp-sf-analytics-deep-link a { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.mailchimp-sf-analytics-deep-link a:hover { - color: var(--mailchimp-color-text, #1d2327); -} - -.mailchimp-sf-analytics-deep-link .dashicons { - font-size: 16px; - width: 16px; - height: 16px; - line-height: 16px; -} - -/* Responsive */ -@media screen and (max-width: 782px) { - .mailchimp-sf-analytics-filters { - flex-direction: column; - align-items: stretch; - } - - .mailchimp-sf-date-picker-popover { - min-width: auto; - width: calc(100vw - 60px); - left: 0; - } - - .mailchimp-sf-date-picker-popover-row { - flex-direction: column; - } - - .mailchimp-sf-date-picker-trigger { - width: 100%; - min-width: auto; - } - - .mailchimp-sf-analytics-filter-group select { - min-width: auto; - width: 100%; - } -} - -/* Datepicker */ -.datepicker-cell.selected, .datepicker-cell.selected:hover { - background-color: var(--mailchimp-color-link, #017e89); - color: #fff; -} diff --git a/assets/css/analytics.scss b/assets/css/analytics.scss new file mode 100644 index 00000000..f4470f99 --- /dev/null +++ b/assets/css/analytics.scss @@ -0,0 +1,770 @@ +/** + * Mailchimp Analytics Page Styles + * + * @package Mailchimp + */ + +// ----------------------------------------------------------------------------- +// Breakpoints. +// ----------------------------------------------------------------------------- +$mq-small: 782px; +$mq-medium: 900px; +$mq-xs: 600px; + +// ----------------------------------------------------------------------------- +// Filters toolbar +// ----------------------------------------------------------------------------- +.mailchimp-sf-analytics-filters { + align-items: flex-end; + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 20px; +} + +.mailchimp-sf-analytics-filter-group { + display: flex; + flex-direction: column; + gap: 4px; + + > label { + color: #1d2327; + font-size: 14px; + font-weight: 400; + } + + select { + background-color: #fff; + border: 1px solid #8c8f94; + border-radius: 4px; + color: #1d2327; + font-size: 14px; + height: 36px; + min-width: 180px; + padding: 0 8px; + + &:focus { + border-color: var(--mailchimp-color-link, #017e89); + box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); + outline: none; + } + } +} + +// ----------------------------------------------------------------------------- +// Date picker trigger + popover +// ----------------------------------------------------------------------------- +.mailchimp-sf-date-picker { + position: relative; + + &.is-open .mailchimp-sf-date-picker-trigger { + background-color: rgba(1, 126, 137, 0.08); + border-color: var(--mailchimp-color-link, #017e89); + box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); + } +} + +.mailchimp-sf-date-picker-trigger { + align-items: center; + background-color: #fff; + border: 1px solid #c3ced5; + border-radius: 6px; + color: var(--mailchimp-color-text); + cursor: pointer; + display: inline-flex; + font-size: 14px; + gap: 8px; + height: 40px; + justify-content: space-between; + min-width: 220px; + padding: 0 10px; + text-align: left; + transition: all 0.2s; + + &:hover { + border-color: var(--mailchimp-color-link, #017e89); + } + + &:focus { + border-color: var(--mailchimp-color-link, #017e89); + box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); + outline: none; + } + + .dashicons { + color: #50575e; + font-size: 18px; + height: 18px; + line-height: 18px; + width: 18px; + } + + .indicator-date-picker { + bottom: 0; + display: flex; + justify-content: center; + padding: 0 8px; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + width: 24px; + } +} + +.mailchimp-sf-date-picker-popover { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + display: none; + left: 0; + min-width: 480px; + padding: 20px; + position: absolute; + top: calc(100% + 6px); + z-index: 100; + + &.is-open { + display: block; + } +} + +.mailchimp-sf-date-picker-popover-row { + display: flex; + gap: 12px; + margin-bottom: 16px; +} + +.mailchimp-sf-date-picker-field { + display: flex; + flex: 1; + flex-direction: column; + gap: 6px; + + label { + color: #4a5565; + font-size: 14px; + font-weight: 400; + } +} + +.mailchimp-sf-date-picker-input-wrap { + position: relative; + width: 100%; +} + +.mailchimp-sf-date-picker-field-calendar { + align-items: center; + bottom: 0; + display: flex; + justify-content: center; + padding: 0 6px; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + + svg { + display: block; + fill: #50575e; + height: 18px; + width: 18px; + } +} + +#mailchimp-sf-settings-page .mailchimp-sf-date-picker-field { + + select, + input[type="text"] { + background-color: #fff; + color: #1d2327; + font-size: 14px; + height: 36px; + width: 100%; + + &:focus { + border-color: var(--mailchimp-color-link, #017e89); + box-shadow: 0 0 0 1px var(--mailchimp-color-link, #017e89); + outline: none; + } + } + + .mailchimp-sf-date-picker-input-wrap input[type="text"] { + padding-right: 30px; + } +} + +// Popover actions — pill shape to match design +.mailchimp-sf-date-picker-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +// ----------------------------------------------------------------------------- +// Content area +// ----------------------------------------------------------------------------- +.mailchimp-sf-analytics-content { + background: #fff; + border: 1px solid #d3d0c8; + border-radius: 8px; + box-sizing: border-box; + margin-bottom: 20px; + min-height: 200px; + overflow: auto; + padding: 24px; + width: 100%; +} + +.mailchimp-sf-analytics-placeholder { + align-items: center; + color: #8c8f94; + display: flex; + font-size: 15px; + justify-content: center; + min-height: 200px; +} + +// ----------------------------------------------------------------------------- +// Deep link +// ----------------------------------------------------------------------------- +.mailchimp-sf-analytics-deep-link { + margin-top: 16px; + + a { + align-items: center; + display: inline-flex; + gap: 6px; + + &:hover { + color: var(--mailchimp-color-text, #1d2327); + } + } + + .dashicons { + font-size: 16px; + height: 16px; + line-height: 16px; + width: 16px; + } +} + +// ----------------------------------------------------------------------------- +// Datepicker calendar cell +// ----------------------------------------------------------------------------- +.datepicker-cell.selected, +.datepicker-cell.selected:hover { + background-color: var(--mailchimp-color-link, #017e89); + color: #fff; +} + +// ----------------------------------------------------------------------------- +// Analytics card (shared shell for chart sections) +// ----------------------------------------------------------------------------- +.mailchimp-sf-analytics-card { + // Color tokens are defined on the card so any descendant can reference them + // and theming can override at the card level without leaking to the admin. + --mc-sa-text-strong: #241c15; + --mc-sa-text-muted: #464e54; + --mc-sa-text-body: #374151; + --mc-sa-border: #dddddd; + --mc-sa-skeleton-grid: #F4F4F5; + --mc-sa-blue: #2b72fb; + --mc-sa-red: #fa4b42; + --mc-sa-grey: #9ca3af; + --mc-sa-notice-bg: #eff6ff; + --mc-sa-notice-border: #bfdbfe; + --mc-sa-notice-text: #1e3a8a; + --mc-sa-error-bg: #fef2f2; + --mc-sa-error-border: #f25f25; + --mc-sa-error-text: #d03e04; + --mc-sa-error-title: #d03e04; + + background: #fff; + border: 1px solid var(--mc-sa-border); + border-radius: 12px; + box-sizing: border-box; + margin-bottom: 20px; + padding: 24px; + position: relative; + + &__header { + border-bottom: 1px solid var(--mc-sa-border); + margin-bottom: 20px; + padding-bottom: 16px; + } + + &__title { + color: var(--mc-sa-text-strong); + font-size: 24px; + font-weight: 500; + line-height: 1.25; + margin: 0 0 4px; + } + + &__subtitle { + color: var(--mc-sa-text-muted); + font-size: 14px; + line-height: 1.35; + margin: 0; + } +} + +// ----------------------------------------------------------------------------- +// Subscriber activity section +// ----------------------------------------------------------------------------- +.mailchimp-sf-sa { + + // Body grid — chart column + totals column + &__body { + align-items: stretch; + display: grid; + gap: 32px; + grid-template-columns: minmax(0, 1fr) 320px; + margin-top: 32px; + + &[hidden] { + display: none; + } + } + + // Chart column + &__chart { + min-width: 0; + } + + &__chart-title { + color: var(--mc-sa-text-strong); + font-size: 16px; + font-weight: 500; + margin: 0 0 4px; + } + + &__chart-subtitle { + color: var(--mc-sa-text-muted); + font-size: 12px; + font-weight: 400; + line-height: 1.35; + margin: 0 0 16px; + } + + // Canvas area (hosts the real canvas + skeleton + overlay + background grid) + &__canvas-wrap { + height: 340px; + position: relative; + width: 100%; + + canvas { + display: block; + height: 100% !important; + width: 100% !important; + } + } + + &__canvas { + position: relative; + transition: opacity 150ms ease; + z-index: 1; + } + + // Totals column + &__totals { + border-left: 1px solid var(--mc-sa-border); + display: flex; + flex-direction: column; + padding-left: 32px; + } + + &__totals-title { + border-bottom: 1px solid var(--mc-sa-border); + color: var(--mc-sa-text-strong); + font-size: 14px; + font-weight: 500; + margin: 0 0 20px; + padding-bottom: 12px; + } + + &__donut-wrap { + height: 160px; + margin: 8px auto 24px; + position: relative; + width: 160px; + + canvas { + display: block; + height: 100% !important; + width: 100% !important; + } + } + + &__donut-center { + align-items: center; + display: flex; + inset: 0; + justify-content: center; + pointer-events: none; + position: absolute; + } + + &__net { + color: var(--mc-sa-text-strong); + font-size: 32px; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1; + } + + // Legend + &__legend { + display: flex; + flex-direction: column; + gap: 10px; + list-style: none; + margin: 0; + padding: 0; + } + + &__legend-item { + align-items: center; + color: var(--mc-sa-text-body); + display: grid; + font-size: 13px; + gap: 10px; + grid-template-columns: 14px 1fr auto; + margin: 0; + + &.is-new .mailchimp-sf-sa__legend-swatch { + background: var(--mc-sa-blue); + } + + &.is-unsub .mailchimp-sf-sa__legend-swatch { + background: var(--mc-sa-red); + } + } + + &__legend-swatch { + background: var(--mc-sa-grey); + border-radius: 3px; + display: inline-block; + height: 14px; + width: 14px; + } + + &__legend-label { + color: var(--mc-sa-text-body); + } + + &__legend-value { + color: var(--mc-sa-text-strong); + font-weight: 600; + text-align: right; + } + + // Notice (e.g. 180-day limited) + &__notice { + background: var(--mc-sa-notice-bg); + border: 1px solid var(--mc-sa-notice-border); + border-left-width: 4px; + border-radius: 6px; + color: var(--mc-sa-notice-text); + font-size: 13px; + line-height: 1.4; + margin-bottom: 20px; + padding: 10px 14px; + } + + /* + * Skeleton bars — live in the top ~60% of the canvas-wrap so the overlay + * copy below isn't covered. No background grid here; the grid lives on + * canvas-wrap itself so it spans the full chart area, not just the bars. + */ + &__skeleton-bars { + align-items: flex-end; + bottom: 42%; + display: none; + gap: 16px; + justify-content: center; + left: 0; + padding: 4px 0 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + + span { + background: var(--mc-sa-border); + border-radius: 2px; + display: block; + width: 18px; + } + + span:nth-child(1) { + height: 96px; + } + + span:nth-child(2) { + height: 53px; + } + + span:nth-child(3) { + height: 122px; + } + + span:nth-child(4) { + height: 80px; + } + + span:nth-child(5) { + height: 40px; + } + } + + // Skeleton donut — outlined grey ring, same size as the real donut + &__skeleton-donut { + border: 20px solid var(--mc-sa-border); + border-radius: 50%; + box-sizing: border-box; + display: none; + height: 100%; + inset: 0; + pointer-events: none; + position: absolute; + width: 100%; + } + + /* + * Overlay message — sits in the bottom portion of the chart area, below + * the skeleton bars. Top padding gives breathing room between the bars' + * baseline and the copy. + */ + &__overlay { + align-items: flex-start; + bottom: 0; + color: #666666; + display: none; + font-size: 14px; + font-weight: 500; + height: 40%; + justify-content: center; + left: 0; + padding: 28px 16px 0; + pointer-events: none; + position: absolute; + right: 0; + text-align: center; + z-index: 2; + } + + // ------------------------------------------------------------------------- + // Error banner (shown in place of the chart overlay in the error state). + // ------------------------------------------------------------------------- + &__error-banner { + align-items: center; + background: var(--mc-sa-error-bg); + border: 1px solid var(--mc-sa-error-border); + border-radius: 8px; + display: flex; + gap: 12px; + margin: 4px 0 16px; + padding: 12px 14px; + + &[hidden] { + display: none; + } + } + + &__error-banner-icon { + align-items: center; + color: var(--mc-sa-error-text); + display: inline-flex; + flex-shrink: 0; + justify-content: center; + line-height: 0; + } + + &__error-banner-body { + flex: 1 1 auto; + min-width: 0; + } + + &__error-banner-title { + color: var(--mc-sa-error-title); + font-size: 13px; + font-weight: 600; + line-height: 1.3; + margin: 0 0 2px; + } + + &__error-banner-message { + color: var(--mc-sa-error-text); + font-size: 13px; + line-height: 1.4; + margin: 0; + } + + &__error-banner-action { + flex-shrink: 0; + } + + // ------------------------------------------------------------------------- + // State modifiers: loading / empty / error + // Loading + empty share the same skeleton presentation; error shows the + // banner instead of the chart overlay but keeps the skeleton chrome. + // ------------------------------------------------------------------------- + &.is-loading, + &.is-empty, + &.is-error { + + // Fade the real canvases so skeletons show through. + .mailchimp-sf-sa__canvas { + opacity: 0; + } + + // Full-width background grid on the canvas-wrap; visible across the + // entire chart area (not only where the bars live). + .mailchimp-sf-sa__canvas-wrap { + background-image: + linear-gradient(to right, var(--mc-sa-skeleton-grid) 1px, transparent 1px), + linear-gradient(to bottom, var(--mc-sa-skeleton-grid) 1px, transparent 1px); + background-position: 0 100%; + background-size: calc(100% / 7) calc(100% / 8); + } + + .mailchimp-sf-sa__skeleton-bars, + .mailchimp-sf-sa__skeleton-donut { + display: flex; + } + + // Muted legend + center value while no real data is available. + .mailchimp-sf-sa__net, + .mailchimp-sf-sa__legend-label, + .mailchimp-sf-sa__legend-value { + color: var(--mc-sa-grey); + opacity: 0.85; + } + + .mailchimp-sf-sa__legend-swatch { + background: var(--mc-sa-border) !important; + } + } + + // The chart overlay is used for loading + empty messages only. In the + // error state the banner above the body communicates the problem. + &.is-loading, + &.is-empty { + + .mailchimp-sf-sa__overlay { + display: flex; + } + } + + // Pulse animation — skeleton + overlay "flash" while data is fetching. + &.is-loading { + + .mailchimp-sf-sa__skeleton-bars span, + .mailchimp-sf-sa__skeleton-donut, + .mailchimp-sf-sa__overlay { + animation: mailchimp-sf-sa-pulse 1.4s ease-in-out infinite; + } + } + + button#mailchimp-sf-sa-error-retry { + background-color: #fff; + border: 1px solid #ccd6dc; + + &:hover, + &:focus, + &:active { + // color: var(--mailchimp-color-link); + background-color: #f6f7f7; + } + } +} + +@keyframes mailchimp-sf-sa-pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.45; + } +} + +@media (prefers-reduced-motion: reduce) { + + .mailchimp-sf-sa.is-loading .mailchimp-sf-sa__skeleton-bars span, + .mailchimp-sf-sa.is-loading .mailchimp-sf-sa__skeleton-donut, + .mailchimp-sf-sa.is-loading .mailchimp-sf-sa__overlay { + animation: none; + } +} + +// ----------------------------------------------------------------------------- +// Responsive +// ----------------------------------------------------------------------------- +@media screen and (max-width: $mq-small) { + + .mailchimp-sf-analytics-filters { + align-items: stretch; + flex-direction: column; + } + + .mailchimp-sf-date-picker-popover { + left: 0; + min-width: auto; + width: calc(100vw - 60px); + } + + .mailchimp-sf-date-picker-popover-row { + flex-direction: column; + } + + .mailchimp-sf-date-picker-trigger { + min-width: auto; + width: 100%; + } + + .mailchimp-sf-analytics-filter-group select { + min-width: auto; + width: 100%; + } +} + +@media screen and (max-width: $mq-medium) { + + .mailchimp-sf-sa__body { + gap: 24px; + grid-template-columns: 1fr; + } + + .mailchimp-sf-sa__totals { + border-left: none; + border-top: 1px solid var(--mc-sa-border); + padding-left: 0; + padding-top: 24px; + } +} + +@media screen and (max-width: $mq-xs) { + + .mailchimp-sf-analytics-card { + padding: 16px; + } + + .mailchimp-sf-sa__canvas-wrap { + height: 240px; + } + + .mailchimp-sf-sa__donut-wrap { + height: 160px; + width: 160px; + } + + .mailchimp-sf-sa__net { + font-size: 28px; + } +} diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 7f1440a1..31c0a7e8 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -9,7 +9,12 @@ */ import { Datepicker } from 'vanillajs-datepicker'; import 'vanillajs-datepicker/css/datepicker.css'; // eslint-disable-line import/no-unresolved -import '../css/analytics.css'; +import '../css/analytics.scss'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; (function () { const dateRangeSelect = document.getElementById('mailchimp-sf-date-range'); @@ -29,12 +34,14 @@ import '../css/analytics.css'; if (dateFrom && dateTo) { fromDatepicker = new Datepicker(dateFrom, { + maxView: 0, format: 'yyyy-mm-dd', autohide: true, maxDate: new Date(), }); toDatepicker = new Datepicker(dateTo, { + maxView: 0, format: 'yyyy-mm-dd', autohide: true, maxDate: new Date(), @@ -397,6 +404,429 @@ import '../css/analytics.css'; fetchAnalyticsData(e.detail); }); + /** + * Subscriber change over time — diverging bar + totals donut. + * Loads independently from other analytics sections so an API error in + * this section does not affect KPIs or Form Performance. + */ + (function subscriberActivityModule() { + const section = document.querySelector('[data-section="subscriber-activity"]'); + if (!section) { + return; + } + + const barCanvas = document.getElementById('mailchimp-sf-sa-bar'); + const donutCanvas = document.getElementById('mailchimp-sf-sa-donut'); + const netEl = document.getElementById('mailchimp-sf-sa-net'); + const totalNewEl = document.getElementById('mailchimp-sf-sa-total-new'); + const totalUnsubsEl = document.getElementById('mailchimp-sf-sa-total-unsubs'); + const dateRangeEl = document.getElementById('mailchimp-sf-sa-daterange'); + const noticeEl = document.getElementById('mailchimp-sf-sa-notice'); + const overlayEl = document.getElementById('mailchimp-sf-sa-overlay'); + const errorBannerEl = document.getElementById('mailchimp-sf-sa-error-banner'); + const errorMessageEl = document.getElementById('mailchimp-sf-sa-error-message'); + const retryBtnEl = document.getElementById('mailchimp-sf-sa-error-retry'); + + const COLORS = { + newFill: 'rgba(96, 165, 250, 0.85)', + newBorder: '#3B82F6', + unsubFill: 'rgba(248, 113, 113, 0.85)', + unsubBorder: '#EF4444', + gridLine: 'rgba(0, 0, 0, 0.06)', + zeroLine: 'rgba(0, 0, 0, 0.25)', + text: '#6B7280', + }; + + const EM_DASH = '\u2014'; + + const STRINGS = { + loadingSubtitle: __('Loading subscriber activity…', 'mailchimp'), + loadingOverlay: __('Loading subscriber activity…', 'mailchimp'), + emptySubtitle: __('No data available for the selected date range', 'mailchimp'), + emptyOverlay: __('No data available for this date range', 'mailchimp'), + errorDefault: __( + 'Unable to load data for the selected date range. Please check your connection and try again.', + 'mailchimp', + ), + limited: __( + 'Mailchimp subscriber activity is only available for the last 180 days. Showing available data.', + 'mailchimp', + ), + newSubscribers: __('New Subscribers', 'mailchimp'), + unsubscribes: __('Unsubscribes', 'mailchimp'), + }; + + const STATE_CLASSES = ['is-loading', 'is-ready', 'is-empty', 'is-error']; + + let barChart = null; + let donutChart = null; + let inFlight = null; + let lastDetail = null; + + function setState(state) { + STATE_CLASSES.forEach(function (cls) { + section.classList.toggle(cls, cls === `is-${state}`); + }); + } + + function setPlaceholderTotals() { + if (netEl) { + netEl.textContent = EM_DASH; + netEl.classList.remove('is-positive', 'is-negative'); + } + if (totalNewEl) { + totalNewEl.textContent = EM_DASH; + } + if (totalUnsubsEl) { + totalUnsubsEl.textContent = EM_DASH; + } + } + + function showNotice(message) { + if (!noticeEl) { + return; + } + if (message) { + noticeEl.textContent = message; + noticeEl.hidden = false; + } else { + noticeEl.textContent = ''; + noticeEl.hidden = true; + } + } + + function setOverlay(text) { + if (overlayEl) { + overlayEl.textContent = text || ''; + } + } + + function setSubtitle(text) { + if (dateRangeEl) { + dateRangeEl.textContent = text || ''; + } + } + + function destroyCharts() { + if (barChart) { + barChart.destroy(); + barChart = null; + } + if (donutChart) { + donutChart.destroy(); + donutChart = null; + } + } + + function formatRangeLabel(from, to) { + try { + const fromDate = new Date(`${from}T00:00:00`); + const toDate = new Date(`${to}T00:00:00`); + const fmt = new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + return `${fmt.format(fromDate)} – ${fmt.format(toDate)}`; + } catch (err) { + return `${from} – ${to}`; + } + } + + function setErrorBanner(visible, message) { + if (!errorBannerEl) { + return; + } + if (visible) { + if (errorMessageEl) { + errorMessageEl.textContent = message || STRINGS.errorDefault; + } + errorBannerEl.hidden = false; + } else { + errorBannerEl.hidden = true; + } + } + + function showLoading() { + destroyCharts(); + showNotice(''); + setErrorBanner(false); + setOverlay(STRINGS.loadingOverlay); + setSubtitle(STRINGS.loadingSubtitle); + setPlaceholderTotals(); + setState('loading'); + } + + function showEmpty() { + destroyCharts(); + setErrorBanner(false); + setOverlay(STRINGS.emptyOverlay); + setSubtitle(STRINGS.emptySubtitle); + setPlaceholderTotals(); + setState('empty'); + } + + function showError(message) { + destroyCharts(); + showNotice(''); + setOverlay(''); + // Keep subtitle showing the last attempted date range if we have one. + if (lastDetail && lastDetail.from && lastDetail.to) { + setSubtitle(formatRangeLabel(lastDetail.from, lastDetail.to)); + } + setPlaceholderTotals(); + setErrorBanner(true, message); + setState('error'); + } + + function renderBar(data) { + if (!barCanvas || typeof window.Chart === 'undefined') { + return; + } + + const labels = data.map(function (row) { + return row.label; + }); + const newSeries = data.map(function (row) { + return row.new_subscribers || 0; + }); + const unsubSeries = data.map(function (row) { + return -Math.abs(row.unsubscribes || 0); + }); + + const config = { + type: 'bar', + data: { + labels, + datasets: [ + { + label: STRINGS.unsubscribes, + data: unsubSeries, + backgroundColor: COLORS.unsubFill, + borderColor: COLORS.unsubBorder, + borderWidth: 0, + borderRadius: 0, + borderSkipped: false, + maxBarThickness: 32, + }, + { + label: STRINGS.newSubscribers, + data: newSeries, + backgroundColor: COLORS.newFill, + borderColor: COLORS.newBorder, + borderWidth: 0, + borderRadius: 0, + borderSkipped: false, + maxBarThickness: 32, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { + position: 'top', + align: 'center', + labels: { + usePointStyle: true, + pointStyle: 'rectRounded', + boxWidth: 10, + boxHeight: 10, + padding: 16, + color: COLORS.text, + }, + }, + tooltip: { + callbacks: { + label(ctx) { + const value = Math.abs(ctx.parsed.y || 0); + return `${ctx.dataset.label}: ${value}`; + }, + }, + }, + }, + scales: { + x: { + grid: { + color: COLORS.gridLine, + drawBorder: false, + drawOnChartArea: true, + drawTicks: false, + }, + ticks: { color: COLORS.text }, + }, + y: { + beginAtZero: true, + grid: { + color(ctx) { + return ctx.tick && ctx.tick.value === 0 + ? COLORS.zeroLine + : COLORS.gridLine; + }, + drawBorder: false, + drawOnChartArea: true, + }, + ticks: { + color: COLORS.text, + callback(value) { + return value; + }, + }, + }, + }, + }, + }; + + barChart = new window.Chart(barCanvas.getContext('2d'), config); + } + + function renderDonut(totalNew, totalUnsubs) { + if (!donutCanvas || typeof window.Chart === 'undefined') { + return; + } + const total = (totalNew || 0) + (totalUnsubs || 0); + const data = total > 0 ? [totalNew || 0, totalUnsubs || 0] : [1, 0]; + const colors = + total > 0 + ? [COLORS.newBorder, COLORS.unsubBorder] + : ['rgba(0, 0, 0, 0.08)', 'rgba(0, 0, 0, 0.08)']; + + donutChart = new window.Chart(donutCanvas.getContext('2d'), { + type: 'doughnut', + data: { + labels: [STRINGS.newSubscribers, STRINGS.unsubscribes], + datasets: [ + { + data, + backgroundColor: colors, + borderWidth: 0, + cutout: '78%', + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { enabled: total > 0 }, + }, + }, + }); + } + + function renderTotals(payload) { + const net = payload.net_change || 0; + if (netEl) { + const sign = net > 0 ? '+' : ''; + netEl.textContent = `${sign}${net}`; + netEl.classList.toggle('is-positive', net > 0); + netEl.classList.toggle('is-negative', net < 0); + } + if (totalNewEl) { + totalNewEl.textContent = String(payload.total_new || 0); + } + if (totalUnsubsEl) { + totalUnsubsEl.textContent = String(payload.total_unsubs || 0); + } + } + + function render(payload, fromLabel, toLabel) { + destroyCharts(); + setErrorBanner(false); + + if (!Array.isArray(payload.data) || payload.data.length === 0) { + showEmpty(); + return; + } + + showNotice(payload.limited ? STRINGS.limited : ''); + setSubtitle(formatRangeLabel(fromLabel, toLabel)); + setOverlay(''); + setState('ready'); + renderBar(payload.data); + renderDonut(payload.total_new, payload.total_unsubs); + renderTotals(payload); + } + + function fetchActivity(detail) { + if (!window.mailchimpSFAnalytics || !window.mailchimpSFAnalytics.ajax_url) { + showError(); + return; + } + if (!detail || !detail.listId || !detail.from || !detail.to) { + showEmpty(); + return; + } + + lastDetail = { + listId: detail.listId, + from: detail.from, + to: detail.to, + }; + + if (inFlight && typeof inFlight.abort === 'function') { + inFlight.abort(); + } + + const controller = + typeof window.AbortController !== 'undefined' ? new AbortController() : null; + inFlight = controller; + + const formData = new FormData(); + formData.append('action', 'mailchimp_sf_get_subscriber_activity'); + formData.append('nonce', window.mailchimpSFAnalytics.nonce); + formData.append('list_id', detail.listId); + formData.append('date_from', detail.from); + formData.append('date_to', detail.to); + + showLoading(); + + fetch(window.mailchimpSFAnalytics.ajax_url, { + method: 'POST', + body: formData, + credentials: 'same-origin', + signal: controller ? controller.signal : undefined, + }) + .then(function (response) { + return response.json().catch(function () { + return null; + }); + }) + .then(function (body) { + inFlight = null; + if (!body || body.success !== true || !body.data) { + const message = + body && body.data && body.data.message ? body.data.message : ''; + showError(message); + return; + } + render(body.data, detail.from, detail.to); + }) + .catch(function (err) { + if (err && err.name === 'AbortError') { + return; + } + inFlight = null; + showError(); + }); + } + + if (retryBtnEl) { + retryBtnEl.addEventListener('click', function () { + if (lastDetail) { + fetchActivity(lastDetail); + } + }); + } + + document.addEventListener('mailchimp-analytics-refresh', function (e) { + fetchActivity(e.detail); + }); + })(); + // Initialize. updateTriggerLabel(); syncDateInputs(); diff --git a/includes/admin/templates/analytics.php b/includes/admin/templates/analytics.php index 9e744eef..efd1bd4a 100644 --- a/includes/admin/templates/analytics.php +++ b/includes/admin/templates/analytics.php @@ -100,21 +100,144 @@ -
+ +