loreal-global-kickoff/js/app.js
DJP b412356b4b Add Lookup button and nested folder contents display
Features:
- Added "Lookup" button to trigger Box ID validation manually
- No automatic validation on typing - user must click Lookup
- Recursive folder contents fetching (3 levels deep)
- Collapsible nested folder display with expand/collapse buttons
- Visual hierarchy with indentation and folder icons
- Shows all files within nested folders
- Updated UI with better folder/file organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 14:48:25 -05:00

419 lines
13 KiB
JavaScript

/**
* L'Oréal Box Asset Submission Form - Frontend JavaScript
*/
// State management
let validatedBoxData = null;
let isBoxIdValid = false;
// DOM Elements
const boxIdInput = document.getElementById('boxId');
const boxIdValidation = document.getElementById('boxIdValidation');
const lookupBtn = document.getElementById('lookupBtn');
const submitBtn = document.getElementById('submitBtn');
const assetForm = document.getElementById('assetForm');
const messageDiv = document.getElementById('message');
const previewContainer = document.getElementById('previewContainer');
/**
* Initialize the application
*/
function init() {
// Lookup button event listener
lookupBtn.addEventListener('click', handleLookupClick);
// Allow Enter key in Box ID field to trigger lookup
boxIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleLookupClick();
}
});
// Clear validation state when input changes
boxIdInput.addEventListener('input', () => {
isBoxIdValid = false;
validatedBoxData = null;
submitBtn.disabled = true;
boxIdInput.classList.remove('valid', 'invalid');
boxIdValidation.textContent = '';
boxIdValidation.className = 'validation-message';
});
// Form submission
assetForm.addEventListener('submit', handleFormSubmit);
}
/**
* Handle Lookup button click
*/
function handleLookupClick() {
const boxId = boxIdInput.value.trim();
if (!boxId) {
showMessage('Please enter a Box ID', 'error');
boxIdInput.focus();
return;
}
// Validate immediately
boxIdInput.classList.add('validating');
boxIdValidation.textContent = 'Looking up Box ID...';
boxIdValidation.className = 'validation-message loading';
lookupBtn.disabled = true;
lookupBtn.textContent = 'Looking up...';
validateBoxId(boxId);
}
/**
* Validate Box ID via API
*/
async function validateBoxId(boxId) {
try {
const response = await fetch('validate-box.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ boxId })
});
const result = await response.json();
if (result.success) {
// Valid Box ID
isBoxIdValid = true;
validatedBoxData = result.data;
boxIdInput.classList.remove('validating', 'invalid');
boxIdInput.classList.add('valid');
boxIdValidation.textContent = '✓ Valid Box ID';
boxIdValidation.className = 'validation-message success';
// Enable submit button if all other fields are filled
checkFormValidity();
// Show preview
showPreview(result.data);
} else {
// Invalid Box ID
isBoxIdValid = false;
validatedBoxData = null;
boxIdInput.classList.remove('validating', 'valid');
boxIdInput.classList.add('invalid');
boxIdValidation.textContent = '✗ ' + (result.error || 'Invalid Box ID');
boxIdValidation.className = 'validation-message error';
submitBtn.disabled = true;
// Show placeholder
showPreviewPlaceholder();
}
} catch (error) {
console.error('Validation error:', error);
boxIdInput.classList.remove('validating', 'valid');
boxIdInput.classList.add('invalid');
boxIdValidation.textContent = '✗ Error validating Box ID';
boxIdValidation.className = 'validation-message error';
submitBtn.disabled = true;
showPreviewPlaceholder();
} finally {
// Re-enable lookup button
lookupBtn.disabled = false;
lookupBtn.textContent = 'Lookup';
}
}
/**
* Show preview placeholder
*/
function showPreviewPlaceholder() {
previewContainer.innerHTML = `
<div class="preview-placeholder">
<svg width="100" height="100" viewBox="0 0 100 100" fill="none">
<path d="M50 10L80 30V70L50 90L20 70V30L50 10Z" stroke="#FFC407" stroke-width="3" fill="none"/>
<path d="M50 35V65M35 50H65" stroke="#FFC407" stroke-width="3" stroke-linecap="round"/>
</svg>
<p>Enter a Box ID to preview contents</p>
</div>
`;
}
/**
* Render nested folder structure recursively
*/
function renderFolderContents(folders, files, level = 0) {
let html = '';
// Render folders first
if (folders && folders.length > 0) {
folders.forEach(folder => {
const hasContents = (folder.contents?.folders?.length > 0 || folder.contents?.files?.length > 0);
const folderId = 'folder-' + folder.id;
html += `
<div class="item folder-item" style="padding-left: ${level * 20}px;">
${hasContents ? `
<button type="button" class="folder-toggle" onclick="toggleFolder('${folderId}')">
<span class="toggle-icon">▶</span>
</button>
` : '<span class="folder-spacer"></span>'}
<div class="item-icon">📁</div>
<div class="item-name">${escapeHtml(folder.name)}</div>
</div>
`;
if (hasContents) {
html += `<div class="folder-contents" id="${folderId}" style="display: none;">`;
html += renderFolderContents(folder.contents.folders, folder.contents.files, level + 1);
html += '</div>';
}
});
}
// Render files
if (files && files.length > 0) {
files.forEach(file => {
html += `
<div class="item" style="padding-left: ${level * 20 + 24}px;">
<div class="item-icon">📄</div>
<div class="item-name">${escapeHtml(file.name)}</div>
</div>
`;
});
}
return html;
}
/**
* Toggle folder expansion
*/
function toggleFolder(folderId) {
const folderContents = document.getElementById(folderId);
const toggleBtn = folderContents.previousElementSibling.querySelector('.folder-toggle');
const toggleIcon = toggleBtn.querySelector('.toggle-icon');
if (folderContents.style.display === 'none') {
folderContents.style.display = 'block';
toggleIcon.textContent = '▼';
} else {
folderContents.style.display = 'none';
toggleIcon.textContent = '▶';
}
}
// Make toggleFolder available globally
window.toggleFolder = toggleFolder;
/**
* Show Box preview
*/
function showPreview(data) {
const { folderName, masterCampaignNumber, masterCampaignId, contents } = data;
const { total, folders, files } = contents;
const folderContentsHtml = renderFolderContents(folders, files);
previewContainer.innerHTML = `
<div class="preview-content">
<div class="preview-header">
<h2>Box Preview</h2>
</div>
<div class="preview-info">
<div class="preview-info-label">Master Campaign Number</div>
<div class="preview-info-value">${escapeHtml(masterCampaignNumber)}</div>
</div>
<div class="preview-info">
<div class="preview-info-label">Master Campaign ID</div>
<div class="preview-info-value">${escapeHtml(masterCampaignId || 'N/A')}</div>
</div>
<div class="preview-info">
<div class="preview-info-label">Folder Name</div>
<div class="preview-info-value">${escapeHtml(folderName)}</div>
</div>
<div class="preview-section">
<h3>Contents Summary</h3>
<div class="contents-summary">
<div class="summary-item">
<div class="summary-count">${total}</div>
<div class="summary-label">Total Items</div>
</div>
<div class="summary-item">
<div class="summary-count">${folders.length}</div>
<div class="summary-label">Top-Level Folders</div>
</div>
<div class="summary-item">
<div class="summary-count">${files.length}</div>
<div class="summary-label">Top-Level Files</div>
</div>
</div>
</div>
<div class="preview-section">
<h3>All Contents</h3>
<div class="items-list nested-items">
${folderContentsHtml || '<p class="empty-message">No contents found</p>'}
</div>
</div>
</div>
`;
}
/**
* Check if form is valid and enable/disable submit button
*/
function checkFormValidity() {
const supplyDate = document.getElementById('supplyDate').value;
const liveDate = document.getElementById('liveDate').value;
const endDate = document.getElementById('endDate').value;
// Enable submit only if Box ID is valid and all dates are filled
if (isBoxIdValid && supplyDate && liveDate && endDate) {
submitBtn.disabled = false;
} else {
submitBtn.disabled = true;
}
}
/**
* Handle form submission
*/
async function handleFormSubmit(e) {
e.preventDefault();
// Double-check validation
if (!isBoxIdValid || !validatedBoxData) {
showMessage('Please enter a valid Box ID first', 'error');
return;
}
const supplyDate = document.getElementById('supplyDate').value;
const liveDate = document.getElementById('liveDate').value;
const endDate = document.getElementById('endDate').value;
if (!supplyDate || !liveDate || !endDate) {
showMessage('Please fill in all date fields', 'error');
return;
}
// Disable submit button and show loading state
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
try {
const payload = {
boxId: boxIdInput.value.trim(),
supplyDate: formatDate(supplyDate),
liveDate: formatDate(liveDate),
endDate: formatDate(endDate),
boxData: validatedBoxData
};
const response = await fetch('submit.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
// Success - show message
showMessage(
result.message || 'Form submitted successfully! ' +
(result.webhookResponse?.message || ''),
'success'
);
// Reset form after successful submission
setTimeout(() => {
assetForm.reset();
isBoxIdValid = false;
validatedBoxData = null;
boxIdInput.classList.remove('valid', 'invalid', 'validating');
boxIdValidation.textContent = '';
showPreviewPlaceholder();
submitBtn.disabled = true;
}, 2000);
} else {
// Error from webhook or server
showMessage(
'Submission failed: ' + (result.error || 'Unknown error') +
(result.webhookResponse?.message ? ' - ' + result.webhookResponse.message : ''),
'error'
);
}
} catch (error) {
console.error('Submission error:', error);
showMessage('Error submitting form. Please try again.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
}
}
/**
* Format date to DD/MM/YYYY 00:00
*/
function formatDate(dateString) {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year} 00:00`;
}
/**
* Show message to user
*/
function showMessage(text, type) {
messageDiv.textContent = text;
messageDiv.className = `message ${type}`;
messageDiv.style.display = 'block';
// Auto-hide after 5 seconds
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Add event listeners to date fields to check form validity
document.getElementById('supplyDate').addEventListener('change', checkFormValidity);
document.getElementById('liveDate').addEventListener('change', checkFormValidity);
document.getElementById('endDate').addEventListener('change', checkFormValidity);
// Initialize app when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}