diff --git a/.bin/esbuild.mjs b/.bin/esbuild.mjs index 8cbb2790..7eb7b0b3 100755 --- a/.bin/esbuild.mjs +++ b/.bin/esbuild.mjs @@ -28,6 +28,8 @@ const files = [ { in: 'assets/css/default-calendar-list.css', out: 'default-calendar-list.min' }, { in: 'assets/css/sc-welcome-pg-style.css', out: 'sc-welcome-pg-style.min' }, { in: 'assets/generated/tailwind-output.css', out: 'tailwind.min' }, + { in: 'assets/scss/design-system.scss', out: 'design-system.min' }, + { in: 'assets/scss/connect.scss', out: 'connect.min' }, ]; const defaultConfig = { diff --git a/assets/css/admin-post-settings.scss b/assets/css/admin-post-settings.scss index 69641a04..8cf73ff4 100644 --- a/assets/css/admin-post-settings.scss +++ b/assets/css/admin-post-settings.scss @@ -1,4 +1,38 @@ .post-type-calendar { + /* Mask over Calendar Settings when no Google API key is saved */ + .simcal-settings-content-wrap { + position: relative; + } + + .simcal-settings-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #00000099; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + z-index: 10; + } + + .simcal-settings-mask__message { + font-family: Poppins, sans-serif; + font-weight: 600; + font-size: 30px; + line-height: 42px; + letter-spacing: 0; + text-align: center; + color: #ffffff; + margin: 0; + padding: 0 24px; + } + #post-body-content #titlediv #title-prompt-text { padding: 0 0 0 15px; } diff --git a/assets/design-system.html b/assets/design-system.html new file mode 100644 index 00000000..fa0a692d --- /dev/null +++ b/assets/design-system.html @@ -0,0 +1,889 @@ + + + + + + Design System - Simple Calendar + + + +
+

Design System

+

Simple Calendar design language

+ + +
+

Typography

+ +
+

Headings

+
+ .sc_h1 +

Google Calendar plugin

+
+
+ .sc_h2 +

Google Calendar plugin

+
+
+ .sc_h3 +

Google Calendar plugin

+
+
+ .sc_h4 +

Google Calendar plugin

+
+
+ .sc_h5 +
Google Calendar plugin
+
+
+ .sc_h6 +
Google Calendar plugin
+
+
+ +
+
+
+

Poppins Heading Fonts

+
+
+

ABCDEFGHIJKLMNOPQRSTUVWXYZ

+

abcdefghijklmnopqrstuvwxyz

+

1234567890

+

!@#$%^&*()_+?":{}<>

+
+
+
+
+

Inter Body Fonts

+
+
+

ABCDEFGHIJKLMNOPQRSTUVWXYZ

+

abcdefghijklmnopqrstuvwxyz

+

1234567890

+

!@#$%^&*()_+?":{}<>

+
+
+
+ +
+
+
+

Body Font

+
+
+

+ B1_Talent Talent is a top priority for all startup founders and executives. Jobify + offers a way to completely optimize your entire recruiting process. Find better candidates +

+

+ B2_Lorem Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sit amet + accumsan tellus. In pulvinar elit arcu consectetur adipiscing elit Aliquam sit +

+

+ B3_Lorem Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sit amet + accumsan tellus. In pulvinar elit arcu consectetur adipiscing elit +

+
+
+
+
+ + +
+

Logo

+
+ .sc_logo + + +
+
+ + +
+

Buttons

+ +
+
+ Blue (.sc_btn--blue) + Validate & Save +
+
+ White (.sc_btn--white) + Add New Calendar +
+
+ Light blue (.sc_btn--light-blue) + Rate on Wordpress.org +
+
+ Loading (.sc_btn--blue-loading) + +
+
+ Success (.sc_btn--green) + +
+
+ Disabled (.sc_btn--off_white) + +
+
+ Success icon (.sc_btn_icon--success) + +
+
+ Cancel icon (.sc_btn_icon--error) + +
+
+ Learn More (.sc_btn--blue.sc_btn--sm) + Learn More +
+
+ Buy Addon (.sc_btn--white.sc_btn--sm) + Buy Addon +
+
+
+ + +
+

Colors

+ +
+
+
+

Blue
#1D73BE

+
+
+
+

Dark Blue
#27659C

+
+
+
+

Light Blue
#4097E3

+
+
+
+

Black
#000000

+
+
+
+

Dark Gray
#353535

+
+
+
+

Medium Gray
#797979

+
+
+
+

Warm Gray
#A39E98

+
+
+
+

Extra Light Gray
#DBDBDB

+
+
+
+

Off White
#F6F5F4

+
+
+
+

White
#FFFFFF

+
+
+
+

Green
#50BE1D

+
+
+
+

Red
#BE1D1D

+
+
+
+ + +
+

Input Fields

+ +
+ +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+ + Warning + API Key Not Found + +
+ + +
+ +
+ + +
+
+
+
+ + +
+

Icons

+
+
Document
+
Video
+
Help
+
Support
+ + + +
Checked
+
Crown
+
+ Warning + Error +
+
+ + Connected +
+
+ +
+ You need to purchase your own hosting and domain +
+
+
+
+ + +
+

Link

+ +
+ + +
+

Progress Circles

+
+
+
+
+
+
+ 32%
Ready
+
+
    +
  • + + Watch Tutorial +
  • +
  • + + Add API Key +
  • +
  • + + Add New Calendar +
  • +
+
+
+
+
+
+
+ 66%
Ready
+
+
    +
  • + + Watch Tutorial +
  • +
  • + + Add API Key +
  • +
  • + + Add New Calendar +
  • +
+
+
+
+
+
+
+ 100%
Ready
+
+
    +
  • + + Watch Tutorial +
  • +
  • + + Add API Key +
  • +
  • + + Add New Calendar +
  • +
+
+
+
+ + +
+

Helpful links

+ +
+ + +
+

Google Calendar Plugin Setup

+
+ Talent is a top priority for all startup founders and executives. Jobify offers a way to completely optimize + your entire recruiting process. Find better candidates +
+
+
+ + + + + diff --git a/assets/fonts/Inter-Variable-Italic.ttf b/assets/fonts/Inter-Variable-Italic.ttf new file mode 100644 index 00000000..5b358cdd Binary files /dev/null and b/assets/fonts/Inter-Variable-Italic.ttf differ diff --git a/assets/fonts/Inter-Variable.ttf b/assets/fonts/Inter-Variable.ttf new file mode 100644 index 00000000..4ab79e01 Binary files /dev/null and b/assets/fonts/Inter-Variable.ttf differ diff --git a/assets/images/admin/check.svg b/assets/images/admin/check.svg new file mode 100644 index 00000000..a6cb75fe --- /dev/null +++ b/assets/images/admin/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/admin/checked.svg b/assets/images/admin/checked.svg new file mode 100644 index 00000000..3ada97f4 --- /dev/null +++ b/assets/images/admin/checked.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/clapperboard.svg b/assets/images/admin/clapperboard.svg new file mode 100644 index 00000000..9446c468 --- /dev/null +++ b/assets/images/admin/clapperboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/admin/close.svg b/assets/images/admin/close.svg new file mode 100644 index 00000000..a9112a23 --- /dev/null +++ b/assets/images/admin/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/admin/copy-white.svg b/assets/images/admin/copy-white.svg new file mode 100644 index 00000000..846a8857 --- /dev/null +++ b/assets/images/admin/copy-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/copy.svg b/assets/images/admin/copy.svg new file mode 100644 index 00000000..997684f6 --- /dev/null +++ b/assets/images/admin/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/crown.svg b/assets/images/admin/crown.svg new file mode 100644 index 00000000..cdaaac82 --- /dev/null +++ b/assets/images/admin/crown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/admin/document.svg b/assets/images/admin/document.svg new file mode 100644 index 00000000..33aa8e3b --- /dev/null +++ b/assets/images/admin/document.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/admin/eye-hide-white.svg b/assets/images/admin/eye-hide-white.svg new file mode 100644 index 00000000..ebc3e30a --- /dev/null +++ b/assets/images/admin/eye-hide-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/admin/eye-hide.svg b/assets/images/admin/eye-hide.svg new file mode 100644 index 00000000..d774d945 --- /dev/null +++ b/assets/images/admin/eye-hide.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/eye-white.svg b/assets/images/admin/eye-white.svg new file mode 100644 index 00000000..32ed923e --- /dev/null +++ b/assets/images/admin/eye-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/admin/eye.svg b/assets/images/admin/eye.svg new file mode 100644 index 00000000..15406fe1 --- /dev/null +++ b/assets/images/admin/eye.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/admin/headphone.svg b/assets/images/admin/headphone.svg new file mode 100644 index 00000000..4ffcac5e --- /dev/null +++ b/assets/images/admin/headphone.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/admin/loading.svg b/assets/images/admin/loading.svg new file mode 100644 index 00000000..1eae6b70 --- /dev/null +++ b/assets/images/admin/loading.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/admin/logo.png b/assets/images/admin/logo.png new file mode 100644 index 00000000..909b8383 Binary files /dev/null and b/assets/images/admin/logo.png differ diff --git a/assets/images/admin/question-white-small.svg b/assets/images/admin/question-white-small.svg new file mode 100644 index 00000000..9cc26708 --- /dev/null +++ b/assets/images/admin/question-white-small.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/images/admin/question-white.svg b/assets/images/admin/question-white.svg new file mode 100644 index 00000000..c92ada5f --- /dev/null +++ b/assets/images/admin/question-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/question.svg b/assets/images/admin/question.svg new file mode 100644 index 00000000..235b62fb --- /dev/null +++ b/assets/images/admin/question.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/images/admin/star.svg b/assets/images/admin/star.svg new file mode 100644 index 00000000..8a0543b2 --- /dev/null +++ b/assets/images/admin/star.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/admin/warning.svg b/assets/images/admin/warning.svg new file mode 100644 index 00000000..b174e4c8 --- /dev/null +++ b/assets/images/admin/warning.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/images/pages/connect/welcome-video-placeholder.png b/assets/images/pages/connect/welcome-video-placeholder.png new file mode 100644 index 00000000..aafe6e72 Binary files /dev/null and b/assets/images/pages/connect/welcome-video-placeholder.png differ diff --git a/assets/js/admin.js b/assets/js/admin.js index 15947c4a..84d0cf16 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,6 +1,111 @@ (function (window, undefined) { 'use strict'; + /** + * Password / text toggle for square icon buttons (no document-level listeners). + * Wire each button with onclick calling scPasswordToggle.handleButtonClick(event). + * Markup: type="button" data-sc-password-toggle aria-controls="input_id" + * Optional: data-sc-label-show / data-sc-label-hide (else simcal_connect.strings when present). + */ + (function scRegisterPasswordToggleApi() { + if (window.__scPasswordToggleApi) { + return; + } + window.__scPasswordToggleApi = true; + + function getToggleLabels(btn) { + var localized = window.simcal_connect && window.simcal_connect.strings ? window.simcal_connect.strings : {}; + return { + show: btn.getAttribute('data-sc-label-show') || localized.show_api_key || '', + hide: btn.getAttribute('data-sc-label-hide') || localized.hide_api_key || '', + }; + } + + function scPasswordToggleUpdateIcons(btn, input) { + var imgShow = btn.querySelector('.sc_input_square_show'); + var imgHide = btn.querySelector('.sc_input_square_hide'); + if (!imgShow || !imgHide) { + return; + } + var labels = getToggleLabels(btn); + if (input.type === 'password') { + imgShow.setAttribute('hidden', ''); + imgHide.removeAttribute('hidden'); + if (labels.show) { + btn.setAttribute('aria-label', labels.show); + btn.setAttribute('title', labels.show); + } + } else { + imgShow.removeAttribute('hidden'); + imgHide.setAttribute('hidden', ''); + if (labels.hide) { + btn.setAttribute('aria-label', labels.hide); + btn.setAttribute('title', labels.hide); + } + } + } + + function handleButtonClick(e) { + if (!e || !e.currentTarget) { + return; + } + var btn = e.currentTarget; + if (btn.disabled || !btn.hasAttribute('data-sc-password-toggle')) { + return; + } + var inputId = btn.getAttribute('aria-controls'); + if (!inputId) { + return; + } + var input = document.getElementById(inputId); + if (!input || (input.tagName !== 'INPUT' && input.tagName !== 'TEXTAREA')) { + return; + } + if (input.type !== 'password' && input.type !== 'text') { + return; + } + e.preventDefault(); + e.stopPropagation(); + input.type = input.type === 'password' ? 'text' : 'password'; + scPasswordToggleUpdateIcons(btn, input); + } + + function init(root) { + var scRoot = root || document; + var scButtons = scRoot.querySelectorAll('button[data-sc-password-toggle]'); + for (var i = 0; i < scButtons.length; i++) { + var scBtn = scButtons[i]; + if (scBtn.__scPasswordToggleBound) { + continue; + } + scBtn.__scPasswordToggleBound = true; + scBtn.addEventListener('click', handleButtonClick); + + var scInputId = scBtn.getAttribute('aria-controls'); + if (scInputId) { + var scInput = document.getElementById(scInputId); + if (scInput) { + scPasswordToggleUpdateIcons(scBtn, scInput); + } + } + } + } + + window.scPasswordToggle = { + handleButtonClick: handleButtonClick, + updateIcons: scPasswordToggleUpdateIcons, + init: init, + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + init(document); + }); + } else { + init(document); + } + })(); + jQuery(function ($) { /* ======== * * Tooltips * @@ -78,6 +183,28 @@ $(this).hide(); }); tab.show(); + + // Toggle the settings lock mask only for feed types requiring a Google API key. + var settingsContentWrap = calendarSettings.find('.simcal-settings-content-wrap'), + settingsMask = settingsContentWrap.find('.simcal-settings-mask'), + hasGoogleApiKey = String(settingsContentWrap.data('sc-has-google-api-key')) === '1', + requiredFeedsRaw = settingsContentWrap.attr('data-sc-api-key-required-feeds'), + requiredFeeds = ['google', 'grouped-calendars']; + + if (requiredFeedsRaw) { + try { + requiredFeeds = JSON.parse(requiredFeedsRaw); + } catch (e) { + requiredFeeds = ['google', 'grouped-calendars']; + } + } + + var requiresGoogleApiKey = $.inArray(feed, requiredFeeds) !== -1, + shouldShowMask = requiresGoogleApiKey && !hasGoogleApiKey; + + settingsContentWrap.toggleClass('simcal-settings-content-wrap--masked', shouldShowMask); + settingsMask.attr('aria-hidden', shouldShowMask ? 'false' : 'true'); + settingsMask.toggle(shouldShowMask); a.trigger('click'); }) .trigger('change'); @@ -527,4 +654,236 @@ }); }); }); + + /* ========================================= + * Connect page: eye toggle + API key validation (DOM ready) + * ========================================= */ + jQuery(function ($) { + var _scConnectPageEl = $('#simcal-connect-page'); + if (!_scConnectPageEl.length) { + return; + } + + // Use localized config only (strings come from wp_localize_script). + var connectCfg = window.simcal_connect || { + ajax_url: (window.simcal_admin && window.simcal_admin.ajax_url) || '', + nonce: '', + check_icon_url: '', + strings: {}, + }; + + /* API key eye toggle: handled globally via [data-sc-password-toggle] + aria-controls (see top of file). */ + + var scConnectForm = $('#simcal-settings-page-form'); + if (scConnectForm.length) { + var scInput = $('#sc_google_api_key'); + var scConnectFieldWrap = $('#sc_connect_api_key_wrap'); + var scConnectMsgWrap = $('#sc_connect_api_key_msg_wrap'); + var scmsgError = $('#sc_connect_api_key_msg_error'); + var scmsgSuccess = $('#sc_connect_api_key_msg_success'); + var scValidateBtn = $('[data-sc-connect-validate-btn]'); + var scValidateBtnEl = scValidateBtn.get(0) || null; + var validating = false; + var originalBtnHtml = scValidateBtnEl ? scValidateBtn.html() : ''; + var originalBtnClass = scValidateBtnEl ? scValidateBtn.attr('class') : ''; + var resetTimer = null; + var submitTimer = null; + + function resetVisualState() { + if (resetTimer) { + clearTimeout(resetTimer); + resetTimer = null; + } + if (submitTimer) { + clearTimeout(submitTimer); + submitTimer = null; + } + scConnectFieldWrap.removeClass('sc_input--error sc_input--success'); + scConnectMsgWrap.hide(); + scmsgError.hide(); + scmsgSuccess.hide(); + if (scValidateBtnEl) { + scValidateBtn.attr('class', originalBtnClass); + scValidateBtn.html(originalBtnHtml); + scValidateBtn.prop('disabled', false); + } + } + + function animateProgressToApiKeyCompleted() { + var sccircle = $('#sc_connect_progress_circle'); + var scProgressText = $('#sc_connect_progress_text'); + var scOnBoardingStep = $('#sc_connect_step_api_key'); + if (!sccircle.length) return; + + if (scOnBoardingStep.length && !scOnBoardingStep.hasClass('is_completed')) { + scOnBoardingStep.addClass('is_completed'); + var scCheckBox = scOnBoardingStep.find('.sc_checklist_checkbox'); + if (scCheckBox.length && !scCheckBox.find('img').length) { + scCheckBox.html(''); + } + } + + sccircle.addClass('sc_connect_progress_anim'); + sccircle[0].style.setProperty('--sc-progress', '67'); + if (scProgressText.length) { + scProgressText.text((connectCfg.strings && connectCfg.strings['67_ready']) || ''); + } + setTimeout(function () { + sccircle.removeClass('sc_connect_progress_anim'); + }, 1200); + } + + scInput.on('input', function () { + scConnectForm.removeData('scValidated'); + resetVisualState(); + }); + + scConnectForm.on('submit', function (e) { + if (scConnectForm.data('scValidated') === true) { + return; + } + e.preventDefault(); + if (validating) { + return; + } + resetVisualState(); + var inputEl = (scInput && scInput.length ? scInput[0] : null) || document.getElementById('sc_google_api_key'); + var rawKey = ((inputEl && typeof inputEl.value === 'string' ? inputEl.value : '') || '').trim(); + if (scValidateBtnEl) { + // Show loading state immediately on submit attempt. + scValidateBtn.removeClass('sc_is_finished sc_btn--red').addClass('sc_is_active').prop('disabled', true); + } + if (!rawKey) { + scConnectFieldWrap.removeClass('sc_input--success').addClass('sc_input--error'); + scConnectMsgWrap.show(); + scmsgSuccess.hide(); + scmsgError.show(); + scmsgError + .find('.sc_icon_warning_label') + .text((connectCfg.strings && connectCfg.strings.please_enter_api_key) || ''); + if (scValidateBtnEl) { + scValidateBtn.removeClass('sc_is_active sc_is_finished').addClass('sc_btn--red').prop('disabled', false); + } + resetTimer = setTimeout(resetVisualState, 10000); + return; + } + + validating = true; + scConnectFieldWrap.removeClass('sc_input--error sc_input--success'); + scConnectMsgWrap.hide(); + scmsgError.hide(); + scmsgSuccess.hide(); + + var ajaxNonce = (connectCfg && connectCfg.nonce) || scConnectForm.attr('data-sc-connect-validate-nonce') || ''; + var fallbackAjaxUrl = (function () { + var href = + (typeof globalThis !== 'undefined' && + globalThis.location && + typeof globalThis.location.href === 'string' + ? globalThis.location.href + : document && document.location && typeof document.location.href === 'string' + ? document.location.href + : '') || ''; + var wpAdminPos = href.indexOf('/wp-admin/'); + if (wpAdminPos > -1) { + return href.substring(0, wpAdminPos) + '/wp-admin/admin-ajax.php'; + } + return '/wp-admin/admin-ajax.php'; + })(); + var ajaxCandidates = [ + (connectCfg && connectCfg.ajax_url) || '', + window.ajaxurl || '', + (window.simcal_admin && window.simcal_admin.ajax_url) || '', + fallbackAjaxUrl, + ]; + var ajaxUrls = []; + for (var i = 0; i < ajaxCandidates.length; i++) { + var candidate = String(ajaxCandidates[i] || '').trim(); + if (!candidate || $.inArray(candidate, ajaxUrls) !== -1) { + continue; + } + ajaxUrls.push(candidate); + } + + function validateRequestAtIndex(index) { + $.ajax({ + url: ajaxUrls[index], + type: 'POST', + dataType: 'json', + data: { + action: 'simcal_validate_google_api_key', + nonce: ajaxNonce, + api_key: rawKey, + }, + }) + .done(function (res) { + validating = false; + if (res && res.success) { + scConnectFieldWrap.addClass('sc_input--success'); + scConnectMsgWrap.show(); + scmsgSuccess.show(); + animateProgressToApiKeyCompleted(); + $('#sc_connect_add_calendar_btn').show(); + if (scValidateBtnEl) { + scValidateBtn.removeClass('sc_is_active sc_btn--red').addClass('sc_is_finished').prop('disabled', true); + } + scConnectForm.data('scValidated', true); + submitTimer = setTimeout(function () { + if (scValidateBtnEl) scValidateBtn.prop('disabled', false); + scConnectForm.trigger('submit'); + }, 1000); + } else if (res && res.data && res.data.reason === 'api_keys_not_supported') { + scConnectFieldWrap.addClass('sc_input--success'); + scConnectMsgWrap.show(); + scmsgSuccess.show(); + animateProgressToApiKeyCompleted(); + $('#sc_connect_add_calendar_btn').show(); + if (scValidateBtnEl) { + scValidateBtn.removeClass('sc_is_active sc_btn--red').addClass('sc_is_finished').prop('disabled', true); + } + scConnectForm.data('scValidated', true); + submitTimer = setTimeout(function () { + if (scValidateBtnEl) scValidateBtn.prop('disabled', false); + scConnectForm.trigger('submit'); + }, 10000); + } else { + scConnectFieldWrap.addClass('sc_input--error'); + scConnectMsgWrap.show(); + scmsgError.show(); + if (res && res.data && res.data.message) { + scmsgError.find('.sc_icon_warning_label').text(res.data.message); + } + if (scValidateBtnEl) { + scValidateBtn.removeClass('sc_is_active sc_is_finished').addClass('sc_btn--red').prop('disabled', false); + } + resetTimer = setTimeout(resetVisualState, 10000); + } + }) + .fail(function (xhr) { + var isHtmlResponse = xhr && typeof xhr.responseText === 'string' && xhr.responseText.indexOf('') !== -1; + if (isHtmlResponse && index + 1 < ajaxUrls.length) { + validateRequestAtIndex(index + 1); + return; + } + + validating = false; + scConnectFieldWrap.addClass('sc_input--error'); + scConnectMsgWrap.show(); + scmsgError.show(); + var errorText = 'Unable to validate the API key right now. Please try again.'; + if (isHtmlResponse) { + errorText = 'Validation endpoint returned HTML instead of JSON. Please check WP debug notices and admin-ajax routing.'; + } + scmsgError.find('.sc_icon_warning_label').text(errorText); + if (scValidateBtnEl) { + scValidateBtn.removeClass('sc_is_active sc_is_finished').addClass('sc_btn--red').prop('disabled', false); + } + resetTimer = setTimeout(resetVisualState, 10000); + }); + } + + validateRequestAtIndex(0); + }); + } + }); })(this); diff --git a/assets/scss/connect.scss b/assets/scss/connect.scss new file mode 100644 index 00000000..333a6da4 --- /dev/null +++ b/assets/scss/connect.scss @@ -0,0 +1,369 @@ +.sc_root { + /* ======================================== + CONNECT PAGE - WELCOME STEP + ======================================== */ + .sc_connect_welcome_outer { + /* Outer background frame */ + background: var(--sc-color-off-white); + padding: 75px 0; + display: flex; + justify-content: center; + + .sc_connect_welcome_inner { + /* Inner white card */ + background: var(--sc-color-white); + border-radius: var(--sc-radius-16); + max-width: 960px; + width: 100%; + margin: 0 auto; + padding: 48px 64px 56px; + box-sizing: border-box; + text-align: center; + } + + .sc_connect_welcome_video { + /* Video placeholder container */ + margin: 40px auto 0; + max-width: 948px; + width: 100%; + border-radius: var(--sc-radius-15); + background: #00000026; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 948 / 498; + + img { + display: block; + width: 100%; + height: auto; + } + } + + .sc_connect_welcome_button_wrap { + /* Next button wrapper */ + margin-top: 40px; + text-align: center; + } + } + + /* ======================================== + SHARED CARD + TYPOGRAPHY (used by steps and sidebar) + ======================================== */ + + .sc_connect_label { + font-family: var(--sc-font-heading); + font-weight: var(--sc-font-weight-medium); + font-size: var(--sc-font-small); + line-height: var(--sc-body-small-line-height); + color: var(--sc-color-black); + display: block; + margin-bottom: 8px; + } + + /* ======================================== + STEP: API KEY (connect/steps/api-key.php) + ======================================== */ + .sc_connect_helper_row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; + font-family: var(--sc-font-body); + font-size: var(--sc-font-base); + line-height: var(--sc-body-small-line-height); + color: var(--sc-color-dark-gray); + gap: 12px; + } + + .sc_connect_helper_text { + margin: 0; + } + + .sc_connect_helper_link { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-size: var(--sc-font-base); + line-height: var(--sc-body-small-line-height); + letter-spacing: 0; + color: var(--sc-color-blue); + text-decoration: underline; + } + + .sc_connect_pro_link { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-size: var(--sc-font-base); + line-height: var(--sc-body-small-line-height); + letter-spacing: 0; + color: var(--sc-color-dark-gray); + text-align: right; + text-decoration: underline; + + img { + width: 18px; + height: 18px; + display: block; + } + } + + .sc_connect_form_actions { + margin-top: 20px; + display: flex; + align-items: center; + gap: 16px; + } + + /* Success message styles (green) to match warning pattern */ + .sc_connect_success_wrap { + display: inline-flex; + align-items: center; + gap: 8px; + padding-top: 6px; + + /* Green circle icon for success message */ + .sc_icon--circle-small { + width: var(--sc-icon-size); + height: var(--sc-icon-size); + border-radius: 50%; + background: var(--sc-color-green); + display: inline-flex; + align-items: center; + justify-content: center; + + img { + width: var(--sc-icon-xs-size); + height: var(--sc-icon-xs-size); + } + } + } + + .sc_connect_success_label { + font-family: var(--sc-font-body); + font-size: var(--sc-font-small); + font-weight: var(--sc-font-weight-medium); + color: var(--sc-color-green); + } + + /* Button icon for checked.svg (success) */ + .sc_btn--checked-icon { + background-image: url('../images/admin/check.svg'); + background-size: var(--sc-icon-size) var(--sc-icon-size); + } + + /* ======================================== + STEP: HELPFUL LINKS (connect/steps/api-key.php) + ======================================== */ + .sc_connect_helpful_links { + margin-top: 24px; + + /* Fine‑tune Helpful Links layout under heading on Connect page */ + .sc_section_title { + display: block; + width: 100%; + margin-bottom: 16px; + } + + .sc_helpful_links_cards_wrapper { + margin-top: 0; + display: flex; + flex-wrap: wrap; + gap: 20px; + background: var(--sc-color-white); + border-radius: var(--sc-radius-16); + padding: 24px; + } + } + + /* Helpful links cards (white bg wrapper + grey card with icon) */ + .sc_helpful_links_cards_wrapper { + background: var(--sc-color-white); + border-radius: var(--sc-radius-lg); + padding: 24px; + display: flex; + flex-wrap: wrap; + gap: 20px; + } + + /* Individual helpful link card: light grey rounded card, icon on top, label below (match design-system.html) */ + .sc_helpful_link_card { + width: 185px; + min-height: 174px; + border-radius: var(--sc-radius-16); + background: var(--sc-color-off-white); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + text-decoration: none; + color: var(--sc-color-black); + transition: opacity 0.2s ease; + box-sizing: border-box; + padding: 24px 16px; + + &:hover { + opacity: 0.9; + } + + .sc_icon--circle { + flex-shrink: 0; + } + } + + .sc_helpful_link_card_label { + font-weight: var(--sc-font-weight-medium); + font-size: var(--sc-font-medium-sm); + line-height: 1.3; + text-align: center; + margin: 0; + } + + /* ======================================== + SIDEBAR: PROGRESS + RATING/PRO CARDS (connect/sidebar.php) + ======================================== */ + /* Animate progress value change (33 -> 67). */ + @property --sc-progress { + syntax: ''; + inherits: true; + initial-value: 33; + } + + .sc_progress_circle.sc_connect_progress_anim { + transition: --sc-progress 1.1s ease; + } + + /* When setup is fully complete, force checklist connectors and icons to green */ + .sc_connect_progress_is_complete { + .sc_checklist_item::before { + background-color: var(--sc-color-green); + } + + .sc_checklist_checkbox { + border-color: var(--sc-color-green); + } + } + + /* Sidebar stack for rating + pro cards (shown after progress stays 100% for a day) */ + .sc_connect_sidebar_stack { + display: flex; + flex-direction: column; + gap: 16px; + } + + .sc_connect_rating_card { + background: var(--sc-color-blue); + color: var(--sc-color-white); + } + + .sc_connect_rating_title { + font-family: var(--sc-font-heading); + font-weight: var(--sc-font-weight-semibold); + font-size: 30px; + line-height: var(--sc-h4-line-height); + letter-spacing: 0; + color: var(--sc-color-white); + margin: 0 0 8px; + } + + .sc_connect_rating_subtitle { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-size: var(--sc-font-medium-sm); + line-height: var(--sc-body-medium-sm-line-height); + letter-spacing: 0; + color: var(--sc-color-white); + margin: 0 0 16px; + } + + .sc_connect_rating_stars { + display: flex; + gap: 4px; + margin-bottom: 16px; + } + + .sc_connect_rating_star { + width: 18px; + height: 18px; + display: block; + } + + .sc_connect_rating_btn { + margin-top: 4px; + } + + .sc_connect_pro_card { + background: var(--sc-color-white); + } + + .sc_connect_pro_title { + font-family: var(--sc-font-heading); + font-weight: var(--sc-font-weight-semibold); + font-size: var(--sc-font-large); + line-height: 1.23; + color: var(--sc-color-black); + margin: 0 0 8px; + } + + .sc_connect_pro_subtitle { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-size: var(--sc-font-medium-sm); + line-height: var(--sc-body-medium-sm-line-height); + color: var(--sc-color-dark-gray); + margin: 0 0 16px; + } + + .sc_connect_pro_list { + list-style: none; + margin: 0 0 16px; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + + li { + display: flex; + align-items: center; + gap: 8px; + } + } + + .sc_connect_pro_list_icon { + width: 20px; + height: 20px; + border-radius: 50%; + background: #1d73be; + display: inline-flex; + align-items: center; + justify-content: center; + + img { + width: var(--sc-icon-xs-size); + height: var(--sc-icon-xs-size); + display: block; + } + } + + .sc_connect_pro_list_text { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-medium); + font-size: var(--sc-font-medium-sm); + line-height: var(--sc-body-medium-sm-line-height); + color: var(--sc-color-dark-gray); + } + + .sc_connect_pro_btn { + margin-top: 4px; + } + + @media (max-width: 1024px) { + .sc_connect_page_outer .sc_connect_page_inner { + grid-template-columns: 1fr; + } + } +} diff --git a/assets/scss/design-system.scss b/assets/scss/design-system.scss new file mode 100644 index 00000000..fea6b7ee --- /dev/null +++ b/assets/scss/design-system.scss @@ -0,0 +1,1425 @@ +/* ======================================== + DESIGN SYSTEM - CSS Variables & Tokens + ======================================== */ + +/* Local fonts (bundled with plugin). */ +@font-face { + font-family: 'SC Poppins'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../fonts/Poppins-Regular.ttf') format('truetype'); +} +@font-face { + font-family: 'SC Poppins'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../fonts/Poppins-Medium.ttf') format('truetype'); +} +@font-face { + font-family: 'SC Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('../fonts/Poppins-SemiBold.ttf') format('truetype'); +} +@font-face { + font-family: 'SC Poppins'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../fonts/Poppins-Bold.ttf') format('truetype'); +} +@font-face { + font-family: 'SC Poppins'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('../fonts/Poppins-Black.ttf') format('truetype'); +} + +@font-face { + font-family: 'SC Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/Inter-Variable.ttf') format('truetype'); +} +@font-face { + font-family: 'SC Inter'; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/Inter-Variable-Italic.ttf') format('truetype'); +} + +/* ======================================== + ROOT VARIABLES (scoped to Simple Calendar root) + ======================================== */ + +.sc_root { + /* ===== FONT FAMILIES ===== */ + --sc-font-heading: 'SC Poppins', sans-serif; + --sc-font-body: 'SC Inter', sans-serif; + + /* ===== FONT WEIGHTS ===== */ + --sc-font-weight-thin: 100; + --sc-font-weight-extralight: 200; + --sc-font-weight-light: 300; + --sc-font-weight-regular: 400; + --sc-font-weight-medium: 500; + --sc-font-weight-semibold: 600; + --sc-font-weight-bold: 700; + --sc-font-weight-extrabold: 800; + --sc-font-weight-black: 900; + + /* ===== COLOR PALETTE ===== */ + --sc-color-blue: #1d73be; + --sc-color-dark-blue: #27659c; + --sc-color-blue-hover: #27659c; + --sc-color-white-hover: #f6f5f4; + --sc-color-light-blue: #4097e3; + --sc-color-light-blue-hover: #3580c9; + --sc-color-black: #000000; + --sc-color-dark-gray: #353535; + --sc-color-medium-gray: #797979; + --sc-color-warm-gray: #a39e98; + --sc-color-extra-light-gray: #dbdbdb; + --sc-color-light-gray: #c2c2c2; + --sc-color-off-white: #f6f5f4; + --sc-color-white: #ffffff; + --sc-color-green: #50be1d; + --sc-color-red: #be1d1d; + + /* ===== LAYOUT TOKENS ===== */ + --sc-icon-circle-size: 51px; + + /* Input icon / square sizes */ + --sc-input-icon-size: 29px; + --sc-input-square-box-width: 79px; + --sc-input-square-box-height: 62px; + --sc-link-decoration: underline; + + /* ===== BUTTON LOADING (CodePen-style) ===== */ + --sc-btn-loading-base-duration: 1000ms; + --sc-btn-loading-progress-duration: 3000ms; + --sc-btn-loading-active-bg: var(--sc-color-blue); + --sc-btn-loading-finished-bg: var(--sc-color-green); + --sc-btn-loading-progress-bg: var(--sc-color-light-blue); + + /* ===== BUTTON SIZES ===== */ + --sc-btn-padding-y: calc(var(--sc-spacing-unit) * 2.4); /* 12px */ + --sc-btn-padding-x: calc(var(--sc-spacing-unit) * 4.8); /* 24px */ + --sc-btn-font-size: var(--sc-font-base); + --sc-btn-sm-padding-y: calc(var(--sc-spacing-unit) * 1.6); /* 10px */ + --sc-btn-sm-padding-x: calc(var(--sc-spacing-unit) * 3.2); /* 16px */ + --sc-btn-sm-font-size: 13px; + + --sc-input-value-font-size: 16px; + --sc-input-value-line-height: 29px; + --sc-input-value-font-weight: var(--sc-font-weight-semibold); + --sc-input-value-color: var(--sc-color-dark-gray); + --sc-input-value-letter-spacing: 0; + + /* ===== SIZING TOKENS ===== */ + --sc-radius-sm: 10px; + --sc-radius-md: 15px; + --sc-radius-lg: 16px; + --sc-icon-size: 24px; + --sc-icon-xs-size: 13px; + + /** ------------FONT SIZE VARIABLES-------------*/ + + --sc-font-huge: clamp(3.125rem, 4vw + 1rem, 4.375rem); /* 50px → 70px */ + --sc-font-largest: clamp(2.5rem, 3vw + 1rem, 3.125rem); /* 40px → 50px */ + --sc-font-larger: clamp(2rem, 2.2vw + 1rem, 2.188rem); /* 32px → 35px */ + --sc-font-large: clamp(1.5rem, 1.8vw + 1rem, 1.875rem); /* 24px → 30px */ + --sc-font-medium: clamp(1.125rem, 1.2vw + 0.9rem, 1.375rem); /* 18px → 22px */ + --sc-font-medium-sm: clamp(1rem, 1.1vw + 0.7rem, 1.25rem); /* 16px → 20px */ + --sc-font-small: clamp(0.938rem, 0.8vw + 0.8rem, 0.938rem); /* 15px */ + --sc-font-base: clamp(0.875rem, 0.6vw + 0.8rem, 0.875rem); /* 14px */ + + --sc-font-line-height-static: 1; + --sc-font-line-height-11: 1.1; + --sc-font-line-height-12: 1.2; + --sc-font-line-height-13: 1.3; + --sc-font-line-height-14: 1.4; + --sc-font-line-height-15: 1.5; + --sc-font-line-height-16: 1.6; + --sc-font-line-height-17: 1.7; + --sc-font-line-height-18: 1.8; + --sc-font-line-height-19: 1.9; + --sc-font-line-height-20: 2; + + /* ===== TYPOGRAPHY TOKENS ===== */ + + /* Heading Sizes */ + + --sc-h1-line-height: var(--sc-font-line-height-11); + --sc-h1-weight: var(--sc-font-weight-bold); + + --sc-h2-line-height: var(--sc-font-line-height-static); + --sc-h2-weight: var(--sc-font-weight-medium); + + --sc-h3-line-height: var(--sc-font-line-height-12); + --sc-h3-weight: var(--sc-font-weight-medium); + + --sc-h4-line-height: var(--sc-font-line-height-static); + --sc-h4-weight: var(--sc-font-weight-medium); + + --sc-h5-line-height: var(--sc-font-line-height-14); + --sc-h5-weight: var(--sc-font-weight-medium); + + --sc-h6-line-height: var(--sc-font-line-height-20); + --sc-h6-weight: var(--sc-font-weight-medium); + + /* Body Font Sizes */ + --sc-body-heading-size: var(--sc-font-medium); + --sc-body-heading-line-height: var(--sc-font-line-height-16); + --sc-body-heading-weight: var(--sc-font-weight-regular); + + --sc-body-large-size: var(--sc-font-medium); + --sc-body-large-line-height: var(--sc-font-line-height-16); + --sc-body-large-weight: var(--sc-font-weight-regular); + + --sc-body-medium-size: var(--sc-font-small); + --sc-body-medium-line-height: var(--sc-font-line-height-20); + --sc-body-medium-weight: var(--sc-font-weight-regular); + + --sc-body-medium-sm-size: var(--sc-font-medium-sm); + --sc-body-medium-sm-line-height: var(--sc-font-line-height-16); + --sc-body-medium-sm-weight: var(--sc-font-weight-regular); + + --sc-body-small-size: var(--sc-font-base); + --sc-body-small-line-height: var(--sc-font-line-height-18); + --sc-body-small-weight: var(--sc-font-weight-regular); + + --sc-body-xs-size: 12px; + --sc-body-xs-line-height: 30px; + --sc-body-xs-weight: var(--sc-font-weight-regular); + + /* Letter Spacing */ + --sc-letter-spacing: 0%; + + /* Design system typography display (style guide) */ + --sc-ds-typo-label-size: 20px; + --sc-ds-typo-label-line-height: 95px; + --sc-ds-typo-label-letter-spacing: 0%; + --sc-ds-typo-sample-size: 20px; + --sc-ds-typo-sample-line-height: 95px; + --sc-ds-typo-b1-size: 20px; + --sc-ds-typo-b1-line-height: 30px; + --sc-ds-typo-b2-size: 16px; + --sc-ds-typo-b2-line-height: 25px; + --sc-ds-typo-b3-size: 14px; + --sc-ds-typo-b3-line-height: 22px; + + /** ------------SPACING VARIABLES------------------*/ + --sc-spacing-unit: 0.313rem; /* ≈ 5px */ + + --sc-spacing-0: calc(var(--sc-spacing-unit) * 0); /* 5px */ + --sc-spacing-5: calc(var(--sc-spacing-unit) * 1); /* 5px */ + --sc-spacing-10: calc(var(--sc-spacing-unit) * 2); /* 10px */ + --sc-spacing-15: calc(var(--sc-spacing-unit) * 3); /* 15px */ + --sc-spacing-20: calc(var(--sc-spacing-unit) * 4); /* 20px */ + --sc-spacing-25: calc(var(--sc-spacing-unit) * 5); /* 25px */ + --sc-spacing-40: calc(var(--sc-spacing-unit) * 8); /* 40px */ + + /** ------------BORDER RADIUS VARIABLES-------------*/ + + --sc-radius-rounded: 50%; /* 50% */ + --sc-radius-5: 0.313rem; /* 5px */ + --sc-radius-10: 0.625rem; /* 10px */ + --sc-radius-15: 0.938rem; /* 15px */ + --sc-radius-16: 1rem; /* 16px */ + + /* ======================================== + BASE TYPOGRAPHY STYLES + ======================================== */ + + /* Headings */ + .sc_h1 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h1-weight); + font-size: var(--sc-font-huge); + line-height: var(--sc-h1-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + .sc_h2 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h2-weight); + font-size: var(--sc-font-largest); + line-height: var(--sc-h2-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + .sc_h3 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h3-weight); + font-size: var(--sc-font-larger); + line-height: var(--sc-h3-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + .sc_h4 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h4-weight); + font-size: var(--sc-font-large); + line-height: var(--sc-h4-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + .sc_h5 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h5-weight); + font-size: var(--sc-font-medium); + line-height: var(--sc-h5-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + .sc_h6 { + font-family: var(--sc-font-heading); + font-weight: var(--sc-h6-weight); + font-size: var(--sc-font-small); + line-height: var(--sc-h6-line-height); + letter-spacing: var(--sc-letter-spacing); + color: var(--sc-color-black); + } + + label.sc_h6 { + cursor: pointer; + } + + /* ======================================== + BODY TEXT UTILITY CLASSES + ======================================== */ + .sc_text--body_b1 { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-style: normal; + font-size: var(--sc-ds-typo-b1-size); + line-height: var(--sc-ds-typo-b1-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + color: var(--sc-color-black); + } + + .sc_text--body_b2 { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-style: normal; + font-size: var(--sc-ds-typo-b2-size); + line-height: var(--sc-ds-typo-b2-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + color: var(--sc-color-black); + } + + .sc_text--body_b3 { + font-family: var(--sc-font-body); + font-weight: var(--sc-font-weight-regular); + font-style: normal; + font-size: var(--sc-ds-typo-b3-size); + line-height: var(--sc-ds-typo-b3-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + color: var(--sc-color-black); + } + + /* Body Large - Extra Bold 22px */ + .sc_text--body_large { + font-family: var(--sc-font-body); + font-weight: var(--sc-body-large-weight); + font-size: var(--sc-body-large-size); + line-height: var(--sc-body-large-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + } + + /* Body Medium - Bold 15px */ + .sc_text--body_medium { + font-family: var(--sc-font-body); + font-weight: var(--sc-body-medium-weight); + font-size: var(--sc-body-medium-size); + line-height: var(--sc-body-medium-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + } + + /* Body Small - Black 14px */ + .sc_text--body_small { + font-family: var(--sc-font-body); + font-weight: var(--sc-body-small-weight); + font-size: var(--sc-body-small-size); + line-height: var(--sc-body-small-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + } + + /* Body Extra Small - Medium 12px */ + .sc_text--body_xs { + font-family: var(--sc-font-body); + font-weight: var(--sc-body-xs-weight); + font-size: var(--sc-body-xs-size); + line-height: var(--sc-body-xs-line-height); + letter-spacing: var(--sc-letter-spacing); + vertical-align: middle; + } + + /* ======================================== + COLOR UTILITY CLASSES + ======================================== */ + + /* Text Colors */ + .sc_text--blue { + color: var(--sc-color-blue); + } + .sc_text_light_blue { + color: var(--sc-color-light-blue); + } + .sc_text--black { + color: var(--sc-color-black); + } + .sc_text--dark { + color: var(--sc-color-dark-gray); + } + .sc_text--medium_gray { + color: var(--sc-color-medium-gray); + } + .sc_text--warm_gray { + color: var(--sc-color-warm-gray); + } + .sc_text--extra_light_gray { + color: var(--sc-color-extra-light-gray); + } + .sc_text--light_gray { + color: var(--sc-color-light-gray); + } + .sc_text--off_white { + color: var(--sc-color-off-white); + } + .sc_text--white { + color: var(--sc-color-white); + } + + /* Background Colors */ + .sc_bg--blue { + background-color: var(--sc-color-blue); + } + .sc_bg--light_blue { + background-color: var(--sc-color-light-blue); + } + .sc_bg--black { + background-color: var(--sc-color-black); + } + .sc_bg--dark { + background-color: var(--sc-color-dark-gray); + } + .sc_bg--light { + background-color: var(--sc-color-off-white); + } + .sc_bg--white { + background-color: var(--sc-color-white); + } + .sc_bg--green { + background-color: var(--sc-color-green); + } + .sc_bg--red { + background-color: var(--sc-color-red); + } + + /* Border Colors */ + .sc_border--blue { + border-color: var(--sc-color-blue); + } + .sc_border--gray { + border-color: var(--sc-color-light-gray); + } + .sc_border--gray_light { + border-color: var(--sc-color-light-gray); + } + + /* ======================================== + BUTTONS + ======================================== */ + .sc_btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(var(--sc-spacing-unit) * 1.6); /* 8px */ + font-family: var(--sc-font-body); + font-size: var(--sc-btn-font-size); + font-weight: var(--sc-font-weight-medium); + padding: var(--sc-btn-padding-y) var(--sc-btn-padding-x); + border: none; + border-radius: var(--sc-radius-sm); + cursor: pointer; + transition: background-color 0.2s ease; + text-decoration: none; + } + + .sc_btn--sm { + padding: var(--sc-btn-sm-padding-y) var(--sc-btn-sm-padding-x); + font-size: var(--sc-btn-sm-font-size); + } + + .sc_btn--blue { + background: var(--sc-color-blue); + color: var(--sc-color-white); + + &:hover { + background: var(--sc-color-dark-blue); + } + } + + .sc_btn--white { + background: var(--sc-color-white); + color: var(--sc-color-black); + border: 2px solid var(--sc-color-extra-light-gray); + + &:hover { + background: var(--sc-color-white-hover); + } + } + + .sc_btn--light-blue { + background: var(--sc-color-light-blue); + color: var(--sc-color-white); + + &:hover { + background: var(--sc-color-blue-hover); + } + } + + .sc_btn--blue-loading { + &:hover { + background: var(--sc-color-dark-blue); + } + } + + /* CodePen-style loading button (blue) */ + .sc_btn--blue-loading { + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(var(--sc-spacing-unit) * 1.6); /* 8px */ + font-family: var(--sc-font-body); + font-size: var(--sc-font-base); + font-weight: var(--sc-font-weight-medium); + border-radius: var(--sc-radius-sm); + position: relative; + width: 12.5rem; + padding: calc(var(--sc-spacing-unit) * 4.5) calc(var(--sc-spacing-unit) * 5); + background-color: var(--sc-color-blue); + color: var(--sc-color-white); + overflow: hidden; + transition: background-color var(--sc-btn-loading-base-duration) ease; + + &::before { + position: absolute; + content: ''; + bottom: 0; + left: 0; + width: 0%; + height: 100%; + background-color: var(--sc-btn-loading-progress-bg); + } + + > span { + position: absolute; + line-height: 0; + display: inline-flex; + align-items: center; + justify-content: center; + transition: + top var(--sc-btn-loading-base-duration) ease, + transform var(--sc-btn-loading-base-duration) ease; + } + + > span:nth-of-type(1) { + top: 50%; + transform: translateY(-50%); + } + + > span:nth-of-type(2) { + top: 100%; + transform: translateY(0%); + font-size: 24px; + } + + > span:nth-of-type(3) { + display: none; + } + + &.sc_is_active { + background-color: var(--sc-btn-loading-active-bg); + + &::before { + width: 100%; + transition: width var(--sc-btn-loading-progress-duration) linear; + } + + > span:nth-of-type(1) { + top: -100%; + transform: translateY(-50%); + } + + > span:nth-of-type(2) { + top: 50%; + transform: translateY(-50%); + + .sc_btn--loading-icon { + transform-origin: center center; + animation: sc_btn_loading_spin var(--sc-btn-loading-base-duration) linear infinite; + } + } + } + + &.sc_is_finished { + background-color: var(--sc-btn-loading-finished-bg); + + .sc_btn_submit, + .sc_btn_loading { + display: none; + } + + .sc_btn_check { + display: inline-flex; + top: 50%; + transform: translateY(-50%); + font-size: 24px; + animation: sc_btn_loading_scale 0.5s linear; + } + } + } + + @keyframes sc_btn_loading_spin { + 100% { + transform: rotate(360deg); + } + } + + @keyframes sc_btn_loading_scale { + 0% { + transform: translateY(-50%) scale(10); + } + 50% { + transform: translateY(-50%) scale(0.2); + } + 70% { + transform: translateY(-50%) scale(1.2); + } + 90% { + transform: translateY(-50%) scale(0.7); + } + 100% { + transform: translateY(-50%) scale(1); + } + } + + .sc_btn--green { + background: var(--sc-color-green); + color: var(--sc-color-white); + + &:hover { + opacity: 0.9; + } + } + + .sc_btn--red { + background: var(--sc-color-red); + color: var(--sc-color-white); + + &:hover { + opacity: 0.9; + } + } + + .sc_btn--off_white { + background: var(--sc-color-extra-light-gray); + color: var(--sc-color-medium-gray); + cursor: default; + + &:hover { + opacity: 1; + background: var(--sc-color-extra-light-gray); + } + } + + .sc_btn_icon { + width: 150px; + height: 44px; + padding: 0; + border-radius: var(--sc-radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + transition: opacity 0.2s ease; + } + + .sc_btn_icon--blue { + background: var(--sc-color-blue); + color: var(--sc-color-white); + } + + .sc_btn_icon--light-blue { + background: var(--sc-color-white); + border: 2px solid var(--sc-color-extra-light-gray); + color: var(--sc-color-black); + + &:hover { + background: var(--sc-color-off-white); + } + } + + .sc_btn_icon--success { + background: var(--sc-color-green); + color: var(--sc-color-white); + } + + .sc_btn_icon--error { + background: var(--sc-color-red); + color: var(--sc-color-white); + } + + /* SVG icons for buttons (loading, success, close) */ + .sc_btn--icons { + font-size: 0; + width: 20px; + height: 20px; + display: inline-block; + background-size: 16px 16px; + background-position: center center; + background-repeat: no-repeat; + } + + .sc_btn--check-icon { + background-size: 30px 30px; + background-image: url('../images/admin/check.svg'); + } + + .sc_btn--close_icon { + background-image: url('../images/admin/close.svg'); + } + + .sc_btn--loading-icon { + background-image: url('../images/admin/loading.svg'); + } + + /* ======================================== + INPUT FIELDS (Figma: 1px border, 10px radius, 63px height) + ======================================== */ + .sc_input { + font-family: var(--sc-font-body); + font-size: var(--sc-input-value-font-size); + font-weight: var(--sc-input-value-font-weight); + line-height: var(--sc-input-value-line-height); + letter-spacing: var(--sc-input-value-letter-spacing); + vertical-align: middle; + padding: var(--sc-spacing-10) var(--sc-spacing-20); + width: 100%; + min-height: 63px; + box-sizing: border-box; + background: var(--sc-color-white); + color: var(--sc-input-value-color); + border: 1px solid var(--sc-color-extra-light-gray); + border-radius: var(--sc-radius-sm); + transition: border-color 0.2s ease; + + &.sc_input--dark { + background: var(--sc-color-dark-gray); + color: var(--sc-color-white); + } + + &.sc_input--dark::placeholder { + color: var(--sc-color-light-gray); + } + + &::placeholder { + color: var(--sc-color-medium-gray); + } + + &:focus { + outline: none; + border-color: var(--sc-color-dark-gray); + box-shadow: none; + } + } + + .sc_input_muted { + border: 1px solid var(--sc-color-extra-light-gray); + } + + .sc_input_wrapper { + position: relative; + display: flex; + align-items: center; + + .sc_input { + padding-right: 48px; + } + + &--readonly .sc_input { + background: var(--sc-color-off-white); + } + + /* Icons outside the field (Figma: second and last field) */ + &.sc_input_wrapper--icons-outside { + display: flex; + align-items: stretch; + gap: 0; + } + + /* Square icon box outside: 10px gap, same total width as other inputs */ + &.sc_input_wrapper--square { + gap: 10px; + max-width: 100%; + + .sc_input { + border-right-width: 1px; + border-radius: var(--sc-radius-sm); + } + + .sc_icon--square { + border-radius: var(--sc-radius-sm); + flex-shrink: 0; + } + } + + .sc_input_toggle, + .sc_input_status { + position: static; + transform: none; + flex-shrink: 0; + width: 48px; + min-width: 48px; + height: auto; + align-self: stretch; + border: 1px solid var(--sc-color-dark-gray); + border-left: none; + background: var(--sc-color-white); + border-radius: 0; + transition: + background 0.2s ease, + color 0.2s ease; + + &:hover { + background: var(--sc-color-warm-gray); + color: var(--sc-color-warm-gray); + } + } + + .sc_input_toggle, + .sc_input_status { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + padding: 4px; + color: var(--sc-color-medium-gray); + } + + /* Success state: green border (Figma field 3) */ + &.sc_input--success .sc_input, + .sc_input.sc_input--success { + border-color: var(--sc-color-green); + border-width: 1px; + } + + /* Error state: red border (Figma field 4) */ + &.sc_input--error .sc_input { + border-color: var(--sc-color-red); + border-width: 1px; + } + + &.sc_input--error .sc_input_status { + color: var(--sc-color-red); + } + } + + /* Eye/input icon box: reusable anywhere. Use class .sc_icon--square. + With 4 imgs (show, show_white, hide, hide_white) you get toggle + white icon on hover; with 2 imgs (e.g. show + show_white) or 1 img same look and hover. */ + .sc_icon--square { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--sc-input-square-box-width); + min-width: var(--sc-input-square-box-width); + height: var(--sc-input-square-box-height); + border: none; + border-radius: var(--sc-radius-sm); + background: var(--sc-color-off-white); + cursor: pointer; + padding: 0; + transition: + background 0.2s ease, + opacity 0.2s ease; + + &:hover { + background: var(--sc-color-warm-gray); + } + + img { + width: var(--sc-input-icon-size); + height: var(--sc-input-icon-size); + display: block; + } + + /* White icons: hidden by default (override .sc_icon--square img); show only on hover */ + img.sc_input_square_show_white, + img.sc_input_square_hide_white { + display: none; + } + + &:hover .sc_input_square_show, + &:hover .sc_input_square_hide { + display: none; + } + + &:hover .sc_input_square_show_white, + &:hover .sc_input_square_hide_white { + display: none; + } + + &:hover .sc_input_square_show:not([hidden]) ~ .sc_input_square_show_white { + display: block; + } + + &:hover .sc_input_square_hide:not([hidden]) ~ .sc_input_square_hide_white { + display: block; + } + + img[hidden] { + display: none !important; + } + } + + .sc_input_helper { + font-family: var(--sc-font-body); + font-size: var(--sc-body-small-size); + color: var(--sc-color-red); + margin-top: 8px; + line-height: var(--sc-body-small-line-height); + font-weight: var(--sc-body-small-weight); + } + + .sc_input_label { + display: block; + font-family: var(--sc-font-body); + font-size: 14px; + font-weight: 500; + color: var(--sc-color-black); + margin-bottom: 8px; + } + + /* ======================================== + PROGRESS CIRCLE & CHECKLIST (Figma) + ======================================== */ + .sc_progress_circle { + position: relative; + width: 120px; + height: 120px; + display: flex; + align-items: center; + justify-content: center; + } + + .sc_progress_circle_ring { + position: absolute; + inset: 0; + border-radius: 50%; + border: 12px solid var(--sc-color-extra-light-gray); + background: transparent; + } + + .sc_progress_circle_fill { + position: absolute; + inset: 0; + border-radius: 50%; + background: conic-gradient( + var(--sc-color-blue) 0deg, + var(--sc-color-blue) calc(var(--sc-progress, 0) * 3.6deg), + var(--sc-color-extra-light-gray) calc(var(--sc-progress, 0) * 3.6deg) + ); + } + + .sc_progress_circle_inner { + position: absolute; + inset: 12px; + border-radius: 50%; + background: var(--sc-color-white); + z-index: 1; + } + + .sc_progress_circle--33 { + --sc-progress: 33; + } + .sc_progress_circle--67 { + --sc-progress: 67; + } + + .sc_progress_circle--100 { + .sc_progress_circle_ring { + border-color: var(--sc-color-green); + } + + .sc_progress_circle_fill { + background: var(--sc-color-green); + } + + .sc_progress_circle_inner { + background: var(--sc-color-white); + } + } + + .sc_progress_circle_text { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: var(--sc-font-body); + font-weight: 600; + font-size: 14px; + color: var(--sc-color-dark-gray); + text-align: center; + line-height: 1.2; + } + + .sc_progress_circle--33 .sc_progress_circle_text, + .sc_progress_circle--67 .sc_progress_circle_text { + color: var(--sc-color-dark-gray); + } + + .sc_progress_circle--100 .sc_progress_circle_text { + color: var(--sc-color-dark-gray); + } + + /* Checklist – vertical connector line segments; segment color follows progress (blue/green when completed, gray when pending) */ + .sc_checklist { + list-style: none; + padding: 0; + margin: 0; + position: relative; + padding-left: 10px; + } + + .sc_checklist_item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 6px 0; + font-family: var(--sc-font-body); + font-size: 14px; + color: var(--sc-color-medium-gray); + position: relative; + z-index: 1; + + &::after { + content: ''; + position: absolute; + left: 11px; + top: 27px; + width: 2px; + height: calc(100% - 14px); + background: var(--sc-color-extra-light-gray); + z-index: 0; + } + + &:last-child::after { + display: none; + } + + &.is_completed { + color: var(--sc-color-blue); + + &::after { + background: var(--sc-color-blue); + } + + .sc_checklist_text { + text-decoration: none; + } + + .sc_checklist_checkbox { + background: var(--sc-color-blue); + border-color: var(--sc-color-blue); + color: var(--sc-color-white); + } + + .sc_checklist_link { + color: var(--sc-color-blue); + } + } + + &.sc_checklist--green.is_completed { + &::after { + background: var(--sc-color-green); + } + + .sc_checklist_checkbox { + background: var(--sc-color-green); + border-color: var(--sc-color-green); + } + + &, + .sc_checklist_link { + color: var(--sc-color-green); + } + } + } + + .sc_checklist_checkbox { + flex-shrink: 0; + width: 20px; + height: 20px; + min-width: 20px; + border: 2px solid var(--sc-color-extra-light-gray); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: + border-color 0.2s ease, + background 0.2s ease; + background: var(--sc-color-white); + } + + .sc_checklist_icon { + width: 12px; + height: 12px; + display: block; + } + + .sc_checklist_text { + flex: 1; + } + + .sc_checklist_link { + color: inherit; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +/* ======================================== + CONNECT PAGE - MAIN ONBOARDING SCREEN + Mirrors: connect/layout.php -> steps/* -> connect/sidebar.php + ======================================== */ + .sc_connect_page_outer { + background: var(--sc-color-off-white); + padding: 40px 20px 60px; + + /* Header row */ + .sc_connect_page_header { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0 auto 24px; + max-width: 1160px; + + .sc_connect_page_header_left, + .sc_connect_page_header_right { + display: flex; + align-items: center; + flex: 0 0 auto; + } + + .sc_connect_page_header_right { + margin-left: auto; + } + + .sc_connect_page_header_left .sc_logo_link img { + display: block; + height: 40px; + width: auto; + } + } + + /* Two-column layout: main content + sidebar */ + .sc_connect_page_inner { + max-width: 100%; + width: 100%; + margin: 0; + display: grid; + grid-template-columns: 2fr 1fr; + gap: var(--sc-spacing-20); + align-items: flex-start; + } +} + /* ======================================== + LOGO + ======================================== */ + .sc_logo { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + font-family: var(--sc-font-heading); + font-weight: 600; + font-size: 18px; + color: var(--sc-color-black); + } + + .sc_logo_icon { + width: 40px; + height: 40px; + border-radius: var(--sc-radius-md); + display: flex; + align-items: center; + justify-content: center; + background: var(--sc-color-blue); + color: var(--sc-color-white); + font-size: 16px; + font-weight: 700; + } + + .sc_icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--sc-color-white); + color: var(--sc-color-blue); + border: 1px solid var(--sc-color-extra-light-gray); + } + + /* Icon circle: blue circle with white icon (Figma: 51px, #1D73BE) */ + .sc_icon--circle { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--sc-icon-circle-size); + height: var(--sc-icon-circle-size); + min-width: var(--sc-icon-circle-size); + min-height: var(--sc-icon-circle-size); + border-radius: 50%; + background: var(--sc-color-blue); + padding: 0; + border: none; + + img { + max-width: 31px; + object-fit: contain; + } + } + + .sc_icon--circle-small { + display: inline-flex; + align-items: center; + justify-content: center; + + img { + width: 24px; + height: 24px; + object-fit: contain; + } + } + + .sc_icon_warning_wrap { + display: inline-flex; + align-items: center; + gap: 8px; + padding-top: 6px; + + .sc_icon--circle-small img { + width: 24px; + height: 24px; + } + } + + .sc_icon_warning_label { + font-family: var(--sc-font-body); + font-size: 14px; + font-weight: 600; + color: var(--sc-color-red); + } + + .sc_icon_status_wrap { + display: inline-flex; + align-items: center; + gap: 8px; + } + + .sc_icon_status_dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--sc-color-medium-gray); + + &--connected { + background: var(--sc-color-green); + } + } + + .sc_icon_status_label { + font-family: var(--sc-font-body); + font-size: 14px; + font-weight: 400; + color: var(--sc-color-medium-gray); + + &--connected { + color: var(--sc-color-green); + } + } + + /* Small tooltip / helper icon (question mark) */ + .sc_tooltip--icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--sc-icon-size); + height: var(--sc-icon-size); + border-radius: 3px; + background: var(--sc-color-off-white); + cursor: pointer; + padding: 0; + border: none; + transition: background 0.2s ease; + + &:hover, + &:focus-visible { + background: var(--sc-color-warm-gray); + outline: none; + } + } + + .sc_tooltip--icon_img { + width: var(--sc-icon-xs-size); + height: var(--sc-icon-xs-size); + display: block; + } + + .sc_tooltip--icon_img-hover { + display: none; + } + + .sc_tooltip--icon:hover .sc_tooltip--icon_img, + .sc_tooltip--icon:focus-visible .sc_tooltip--icon_img { + display: none; + } + + .sc_tooltip--icon:hover .sc_tooltip--icon_img-hover, + .sc_tooltip--icon:focus-visible .sc_tooltip--icon_img-hover { + display: block; + } + + .sc_tooltip { + position: relative; + display: inline-flex; + align-items: center; + } + + .sc_tooltip--bubble { + position: absolute; + left: 50%; + bottom: calc(100% + 12px); + transform: translateX(-50%) translateY(4px); + min-width: 260px; + max-width: 320px; + padding: 12px 16px; + background: var(--sc-color-off-white); + border: 1px solid var(--sc-color-extra-light-gray); + border-radius: var(--sc-radius-sm); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + font-family: var(--sc-font-body); + font-size: 14px; + line-height: 1.4; + color: var(--sc-color-dark-gray); + text-align: center; + opacity: 0; + visibility: hidden; + transition: + opacity 0.18s ease, + transform 0.18s ease, + visibility 0.18s ease; + z-index: 10; + + &::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 8px 8px 0 8px; + border-style: solid; + border-color: var(--sc-color-extra-light-gray) transparent transparent transparent; + } + } + + .sc_tooltip:hover .sc_tooltip--bubble, + .sc_tooltip:focus-within .sc_tooltip--bubble { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); + } + + /* ======================================== + LINK + ======================================== */ + .sc_link { + color: var(--sc-color-blue); + text-decoration: var(--sc-link-decoration); + font-family: var(--sc-font-body); + font-size: var(--sc-body-small-size); + + &:hover { + opacity: 0.9; + } + } + + .sc_link_line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + + + .sc_link_line { + margin-top: 8px; + } + } + + .sc_link_separator { + color: var(--sc-color-medium-gray); + font-size: var(--sc-body-small-size); + user-select: none; + } + + .sc_link_muted { + color: var(--sc-color-medium-gray); + text-decoration: var(--sc-link-decoration); + + &:hover { + opacity: 0.9; + } + } + + .sc_setup_card { + background: var(--sc-color-white); + border-radius: var(--sc-radius-lg); + padding: 32px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + } +} + +@media (max-width: 1440px) { + .sc_root { + --sc-container-width: 96vw; + } +} diff --git a/composer.json b/composer.json index 8b63a9b8..53f50509 100644 --- a/composer.json +++ b/composer.json @@ -76,6 +76,9 @@ "cp composer.third-party.dump-autoload third-party/composer.json && cd third-party && composer dump-autoload -vvv --no-plugins --classmap-authoritative --no-interaction && rm composer.json", "cp vendor/composer/autoload_files.php third-party/vendor/composer/autoload_files.php", "rm -rf php-scoper" + ], + "build-admin-css": [ + "npm run build:admin-ui-css" ] } } diff --git a/includes/admin/ajax.php b/includes/admin/ajax.php index 6549ceae..5a842edb 100644 --- a/includes/admin/ajax.php +++ b/includes/admin/ajax.php @@ -38,6 +38,9 @@ public function __construct() // Reset add-ons licenses. add_action('wp_ajax_simcal_reset_add_ons_licenses', [$this, 'reset_licenses']); + + // Validate Google API key (Connect page). + add_action('wp_ajax_simcal_validate_google_api_key', [$this, 'validate_google_api_key']); } /** @@ -205,4 +208,95 @@ public function reset_licenses() wp_send_json_success('success'); } + + /** + * Validate a Google Calendar API key. + * + * Checks whether the provided API key can successfully access + * a known public Google Calendar resource. + * + * @since 3.6.3 + * + * @return void + */ + public function validate_google_api_key() + { + $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; + if (empty($nonce) || !wp_verify_nonce($nonce, 'simcal_connect_validate_google_api_key')) { + wp_send_json_error([ + 'message' => __('Nonce verification failed.', 'google-calendar-events'), + ]); + } + + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => __('You do not have permission to do that.', 'google-calendar-events'), + ]); + } + + $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; + if (empty($api_key)) { + wp_send_json_error([ + 'message' => __('Please enter an API key.', 'google-calendar-events'), + ]); + } + + // A known public calendar id we can query without OAuth. + $public_calendar_id = apply_filters( + 'simcal_validate_api_key_public_calendar_id', + 'en.usa%23holiday%40group.v.calendar.google.com' + ); + + $url = add_query_arg( + [ + 'key' => rawurlencode($api_key), + ], + 'https://www.googleapis.com/calendar/v3/calendars/' . $public_calendar_id + ); + + $response = wp_remote_get($url, [ + 'timeout' => 15, + ]); + + if (is_wp_error($response)) { + wp_send_json_error([ + 'message' => $response->get_error_message(), + ]); + } + + $code = (int) wp_remote_retrieve_response_code($response); + + if (200 === $code) { + wp_send_json_success([ + 'message' => __('API key looks valid and Google Calendar API is reachable.', 'google-calendar-events'), + ]); + } + + $body = wp_remote_retrieve_body($response); + $decoded = json_decode($body, true); + $error_message = ''; + if (is_array($decoded) && isset($decoded['error']['message'])) { + $error_message = (string) $decoded['error']['message']; + } + + // Some Google Calendar endpoints have started rejecting API keys entirely and + // require OAuth tokens instead. In that case, we can't reliably know whether + // the key is valid or not, so we return a special "reason" and let the UI + // treat this as "cannot be validated here" instead of hard failure. + if (401 === $code && $error_message && strpos($error_message, 'API keys are not supported by this API') !== false) { + wp_send_json_error([ + 'message' => $error_message, + 'reason' => 'api_keys_not_supported', + ]); + } + + wp_send_json_error([ + 'message' => $error_message + ? $error_message + : __( + 'Unable to validate the API key. Please check that it is correct and that the Google Calendar API is enabled in Google Cloud.', + 'google-calendar-events' + ), + ]); + } } diff --git a/includes/admin/assets.php b/includes/admin/assets.php index b8510291..361479de 100644 --- a/includes/admin/assets.php +++ b/includes/admin/assets.php @@ -102,6 +102,8 @@ public function load() ['wp-color-picker', 'simcal-select2'], SIMPLE_CALENDAR_VERSION ); + wp_register_style('sc-design-system', $css_path . 'design-system.min.css', [], SIMPLE_CALENDAR_VERSION); + wp_register_style('sc-connect', $css_path . 'connect.min.css', ['sc-design-system'], SIMPLE_CALENDAR_VERSION); wp_register_style( 'simcal-admin-add-calendar', $css_path . 'admin-add-calendar.min.css', @@ -112,8 +114,30 @@ public function load() if (simcal_is_admin_screen() !== false) { wp_enqueue_script('simcal-admin'); wp_localize_script('simcal-admin', 'simcal_admin', simcal_common_scripts_variables()); + // Always expose simcal_connect when admin script loads (JS only uses it when #simcal-connect-page exists). + wp_localize_script('simcal-admin', 'simcal_connect', [ + 'ajax_url' => \SimpleCalendar\plugin()->ajax_url(), + 'nonce' => wp_create_nonce('simcal_connect_validate_google_api_key'), + 'check_icon_url' => SIMPLE_CALENDAR_ASSETS . 'images/admin/check.svg', + 'strings' => [ + 'show_api_key' => __('Show API key', 'google-calendar-events'), + 'hide_api_key' => __('Hide API key', 'google-calendar-events'), + 'please_enter_api_key' => __('Please enter API key', 'google-calendar-events'), + 'api_key_format_invalid' => __('API key format looks invalid', 'google-calendar-events'), + '67_ready' => __('67% Ready', 'google-calendar-events'), + ], + ]); wp_enqueue_style('simcal-admin'); + wp_enqueue_style('sc-design-system'); + + // Connect page specific styles. + $is_connect_page = + ($sc_screen && $sc_screen->id === 'calendar_page_simple-calendar_connect') || + $this->current_page === 'simple-calendar_connect'; + if ($is_connect_page) { + wp_enqueue_style('sc-connect'); + } } else { global $post_type; $screen = get_current_screen(); diff --git a/includes/admin/connect-menu.php b/includes/admin/connect-menu.php new file mode 100644 index 00000000..1e015145 --- /dev/null +++ b/includes/admin/connect-menu.php @@ -0,0 +1,43 @@ + + wp_nonce_field('simcal_save_data', 'simcal_meta_nonce'); + + $feeds_options = get_option('simple-calendar_settings_feeds', []); + $api_key = isset($feeds_options['google']['api_key']) ? trim((string) $feeds_options['google']['api_key']) : ''; + + $feed_types_needing_api_key = apply_filters( + 'simcal_feed_types_requiring_google_api_key', + ['google', 'grouped-calendars', 'grouped-calendar'] + ); + $feed_types_needing_api_key = array_values(array_unique(array_map('sanitize_title', (array) $feed_types_needing_api_key))); + + if ($feed_terms = wp_get_object_terms($post->ID, 'calendar_feed')) { + $current_feed_type = sanitize_title(current($feed_terms)->name); + } else { + $current_feed_type = apply_filters('simcal_default_feed_type', 'google'); + } + + $requires_google_api_key = in_array($current_feed_type, $feed_types_needing_api_key, true); + $show_api_key_mask = $requires_google_api_key && empty($api_key); + $connect_url = admin_url('edit.php?post_type=calendar&page=simple-calendar_connect'); + ?>
- -
-
- - ID); ?> -
-
- ID); ?> - +
+
+ > +

+ +

+ + +
- ID);// Thus advanced panel is always the last one: - ?> -
- - ID); ?> +
    + + +
+
+
+ + ID); ?> +
+
+ ID); ?> + +
+ ID); ?> +
+ + ID); ?> +
diff --git a/includes/admin/pages/components/progress.php b/includes/admin/pages/components/progress.php new file mode 100644 index 00000000..9dd1b7dd --- /dev/null +++ b/includes/admin/pages/components/progress.php @@ -0,0 +1,72 @@ + } + * - string $assets_base + * - bool $force_complete_styles + */ +if (!defined('ABSPATH')) { + exit(); +} + +$percent = isset($progress['percent']) ? (int) $progress['percent'] : 0; +$label = isset($progress['label']) ? (string) $progress['label'] : ''; +$items = isset($progress['items']) && is_array($progress['items']) ? $progress['items'] : []; + +$circle_class = $percent >= 100 ? '100' : ($percent >= 67 ? '67' : '33'); +?> +
+ +

+ +

+ +
+
+
+
+
+
+ + + +
+ +
    + + class=""> + + + + + + + + +
+
+
+
+ diff --git a/includes/admin/pages/connect-controller.php b/includes/admin/pages/connect-controller.php new file mode 100644 index 00000000..bc238c93 --- /dev/null +++ b/includes/admin/pages/connect-controller.php @@ -0,0 +1,79 @@ + 'calendar', + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'fields' => 'ids', +]); +$has_published_calendar = $calendar_query->have_posts(); +wp_reset_postdata(); + +// Track when setup first reached 100% so we can hide the progress card after a day. +$completed_timestamp = (int) get_option('simple-calendar_connect_setup_completed_at', 0); +if ($has_published_calendar && $completed_timestamp <= 0) { + $completed_timestamp = time(); + update_option('simple-calendar_connect_setup_completed_at', $completed_timestamp); +} +$hide_progress_after = DAY_IN_SECONDS; +$should_hide_progress = + $has_published_calendar && $completed_timestamp > 0 && time() - $completed_timestamp >= $hide_progress_after; + +// Decide current step. +$step = 'api_key'; +if ($show_welcome) { + $step = 'welcome'; +} elseif ($has_api_key && !$has_published_calendar) { + $step = 'add_calendar'; +} + +$step_title_map = [ + 'welcome' => __('Welcome', 'google-calendar-events'), + 'api_key' => __('Add API Key', 'google-calendar-events'), + 'add_calendar' => __('Add New Calendar', 'google-calendar-events'), +]; +$step_title = isset($step_title_map[$step]) ? $step_title_map[$step] : __('Connect', 'google-calendar-events'); + +$step_template_map = [ + 'welcome' => SIMPLE_CALENDAR_PATH . 'includes/admin/pages/connect/steps/welcome.php', + 'api_key' => SIMPLE_CALENDAR_PATH . 'includes/admin/pages/connect/steps/api-key.php', + 'add_calendar' => SIMPLE_CALENDAR_PATH . 'includes/admin/pages/connect/steps/add-calendar.php', +]; +$step_template_path = isset($step_template_map[$step]) ? $step_template_map[$step] : $step_template_map['api_key']; + +$context = [ + // Shared. + 'assets_base' => $assets_base, + // Step state. + 'api_key' => $api_key, + 'has_api_key' => $has_api_key, + 'has_published_calendar' => $has_published_calendar, + 'should_hide_progress' => $should_hide_progress, + // Welcome. + 'video_url' => apply_filters('simple_calendar_connect_welcome_video_url', 'https://www.youtube.com/embed/VIDEO_ID'), + // Sidebar. + 'sidebar_template_path' => SIMPLE_CALENDAR_PATH . 'includes/admin/pages/connect/sidebar.php', +]; + +include SIMPLE_CALENDAR_PATH . 'includes/admin/pages/connect/layout.php'; diff --git a/includes/admin/pages/connect/layout.php b/includes/admin/pages/connect/layout.php new file mode 100644 index 00000000..ae1836bf --- /dev/null +++ b/includes/admin/pages/connect/layout.php @@ -0,0 +1,54 @@ + +
+
+
+
+ +
+
+ + + +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ diff --git a/includes/admin/pages/connect/sidebar.php b/includes/admin/pages/connect/sidebar.php new file mode 100644 index 00000000..394d3a77 --- /dev/null +++ b/includes/admin/pages/connect/sidebar.php @@ -0,0 +1,123 @@ + $has_published_calendar ? 100 : ($has_api_key ? 67 : 33), + 'label' => $has_published_calendar + ? __('100% Ready', 'google-calendar-events') + : ($has_api_key + ? __('67% Ready', 'google-calendar-events') + : __('33% Ready', 'google-calendar-events')), + 'items' => [ + [ + 'text' => __('Watch Tutorial', 'google-calendar-events'), + 'completed' => true, + 'icon_src' => $assets_base . 'check.svg', + ], + [ + 'id' => 'sc_connect_step_api_key', + 'text' => __('Add API Key', 'google-calendar-events'), + 'completed' => $has_api_key || $has_published_calendar, + 'icon_src' => $assets_base . 'check.svg', + ], + [ + 'id' => 'sc_connect_step_calendar', + 'text' => __('Add New Calendar', 'google-calendar-events'), + 'completed' => $has_published_calendar, + 'icon_src' => $assets_base . 'check.svg', + ], + ], + ]; + + $force_complete_styles = $has_published_calendar; + include SIMPLE_CALENDAR_PATH . 'includes/admin/pages/components/progress.php'; + return; +} +?> +
+
+

+ +

+

+ +

+
+ + + +
+ + + +
+ +
+

+ +

+

+ +

+
    +
  • + + + + + + +
  • +
  • + + + + + + +
  • +
  • + + + + + + +
  • +
  • + + + + + + +
  • +
+ + + +
+
+ diff --git a/includes/admin/pages/connect/steps/add-calendar.php b/includes/admin/pages/connect/steps/add-calendar.php new file mode 100644 index 00000000..90cdb42a --- /dev/null +++ b/includes/admin/pages/connect/steps/add-calendar.php @@ -0,0 +1,19 @@ + +
+

+ +

+

+ +

+ +
+ + + +
+ + +
+ + + +
+

+ ', + '' + ); ?> +

+ + + + + +
+ +
+ + + > + + +
+
+
+ + + diff --git a/includes/admin/pages/connect/steps/welcome.php b/includes/admin/pages/connect/steps/welcome.php new file mode 100644 index 00000000..5db17b9b --- /dev/null +++ b/includes/admin/pages/connect/steps/welcome.php @@ -0,0 +1,40 @@ + +
+
+

+ +

+

+ +

+ +
+ <?php esc_attr_e('Getting Started with Simple Calendar', 'google-calendar-events'); ?> +
+ +
+
+ + +
+
+
+
+ diff --git a/includes/functions/admin.php b/includes/functions/admin.php index 9e5de4e6..ce367b02 100644 --- a/includes/functions/admin.php +++ b/includes/functions/admin.php @@ -141,6 +141,7 @@ function simcal_is_admin_screen() $screens = [ 'customize', 'calendar', + 'calendar_page_simple-calendar_connect', 'calendar_page_simple-calendar_add_ons', 'calendar_page_simple-calendar_settings', 'calendar_page_simple-calendar_tools', diff --git a/includes/installation.php b/includes/installation.php index f57c5e87..49b1cf57 100644 --- a/includes/installation.php +++ b/includes/installation.php @@ -34,6 +34,9 @@ public static function activate() self::create_terms(); self::create_options(); + // Flag to redirect to the Connect page on first admin load after activation. + update_option('simple-calendar_redirect_to_connect', 1); + // Clear cache on activation. simcal_delete_feed_transients(); diff --git a/includes/main.php b/includes/main.php index 5a7f6a67..7f18a3d7 100644 --- a/includes/main.php +++ b/includes/main.php @@ -113,6 +113,11 @@ public function __construct() // Do update call here. add_action('admin_init', [$this, 'update'], 999); + // Redirect to Connect page after activation (only hook when needed). + if (is_admin() && get_option('simple-calendar_redirect_to_connect')) { + add_action('admin_init', [$this, 'maybe_redirect_to_connect'], 1); + } + // Init hooks. add_action('init', [$this, 'init'], 5); add_action('admin_init', [$this, 'register_settings'], 5); @@ -321,6 +326,41 @@ public static function update() { $update = new Update(SIMPLE_CALENDAR_VERSION); } + + /** + * Redirect to the Connect page on first admin load after activation. + * + * @since 3.6.3 + * + * @return void + */ + public function maybe_redirect_to_connect() + { + // Only run in admin and for users who can manage options. + if (!is_admin() || !current_user_can('manage_options')) { + return; + } + + // Do not redirect during AJAX or if no redirect flag is set. + if ((defined('DOING_AJAX') && DOING_AJAX) || !get_option('simple-calendar_redirect_to_connect')) { + return; + } + + // Avoid redirect on bulk activation. + if (isset($_GET['activate-multi'])) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + delete_option('simple-calendar_redirect_to_connect'); + return; + } + + // Clear the flag so we only redirect once. + delete_option('simple-calendar_redirect_to_connect'); + + $redirect_url = admin_url('edit.php?post_type=calendar&page=simple-calendar_connect'); + + wp_safe_redirect($redirect_url); + exit(); + } } /**