first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
@@ -0,0 +1,41 @@
.app-card {
cursor: pointer;
background: #fff;
transition: border-color .15s ease, box-shadow .15s ease;
border: 1px solid #dee2e6 !important;
}
.app-card:hover {
border-color: #714B67 !important;
}
.app-card-icon {
width: 32px;
height: 32px;
object-fit: contain;
flex-shrink: 0;
}
label.app-card.selected {
border-color: #714B67 !important;
box-shadow: 0 0 0 2px rgba(113, 75, 103, .25);
background: #faf7f9;
}
.app-card-check {
position: absolute;
top: -10px;
right: -10px;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 1px solid #714B67;
color: #714B67;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
#selected_apps_list li {
display: flex;
align-items: center;
gap: 8px;
}
@@ -0,0 +1,116 @@
/** SaaS Trial Portal - Frontend interactions
* Drives the 2-step /trial wizard:
* Step 1: Choose your Apps (card grid + live sidebar)
* Step 2: Your Information (registration form)
*
* NOTE: Odoo bundles/loads frontend JS assets and this script can run
* AFTER DOMContentLoaded has already fired (depending on bundle timing).
* Relying only on the DOMContentLoaded event can mean init() never runs.
* So we check document.readyState first and run immediately if the DOM
* is already parsed, falling back to the event listener otherwise.
*/
(function () {
function init() {
console.log('[saas_trial_portal] trial_form.js init');
var selected = {}; // { appId: appName }
var countEls = [document.getElementById('selected_count'), document.getElementById('selected_count_2')];
var listEl = document.getElementById('selected_apps_list');
var sidebar = document.getElementById('trial_sidebar');
var continueBtn = document.getElementById('btn_continue');
var stepApps = document.getElementById('trial_step_apps');
var stepForm = document.getElementById('trial_step_form');
var changeBtn = document.getElementById('btn_change_selection');
var hiddenAppIds = document.getElementById('app_ids_json');
// Bail out quietly if we're not on the /trial page at all
// (this JS is bundled site-wide via web.assets_frontend).
if (!stepApps) {
return;
}
var cardCount = document.querySelectorAll('.app-card').length;
console.log('[saas_trial_portal] found', cardCount, 'app cards');
function render() {
var ids = Object.keys(selected);
countEls.forEach(function (el) { if (el) { el.textContent = ids.length; } });
if (listEl) {
listEl.innerHTML = '';
ids.forEach(function (id) {
var li = document.createElement('li');
li.className = 'mb-2';
li.textContent = selected[id];
listEl.appendChild(li);
});
}
if (sidebar) {
sidebar.classList.toggle('d-none', ids.length === 0);
}
if (hiddenAppIds) {
hiddenAppIds.value = JSON.stringify(ids);
}
}
// Delegated click handler - resilient to render timing and to
// the native <label for="..."> forwarding clicks to its hidden
// checkbox.
document.addEventListener('click', function (ev) {
var card = ev.target.closest('.app-card');
if (!card) { return; }
ev.preventDefault();
ev.stopPropagation();
var id = card.getAttribute('data-app-id');
var name = card.getAttribute('data-app-name');
var checkbox = card.querySelector('.app-card-checkbox');
var checkMark = card.querySelector('.app-card-check');
if (selected[id]) {
delete selected[id];
card.classList.remove('selected');
if (checkbox) { checkbox.checked = false; }
if (checkMark) { checkMark.classList.add('d-none'); }
} else {
selected[id] = name;
card.classList.add('selected');
if (checkbox) { checkbox.checked = true; }
if (checkMark) { checkMark.classList.remove('d-none'); }
}
console.log('[saas_trial_portal] selected apps:', selected);
render();
});
if (continueBtn) {
continueBtn.addEventListener('click', function () {
if (Object.keys(selected).length === 0) { return; }
stepApps.classList.add('d-none');
stepForm.classList.remove('d-none');
window.scrollTo({top: 0, behavior: 'smooth'});
});
}
if (changeBtn) {
changeBtn.addEventListener('click', function () {
stepForm.classList.add('d-none');
stepApps.classList.remove('d-none');
window.scrollTo({top: 0, behavior: 'smooth'});
});
}
render();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM already parsed by the time this script executed - run now.
init();
}
})();