diff --git a/blocks/carousel/_carousel.json b/blocks/carousel/_carousel.json new file mode 100644 index 0000000..db15056 --- /dev/null +++ b/blocks/carousel/_carousel.json @@ -0,0 +1,104 @@ +{ + "definitions": [ + { + "title": "Carousel", + "id": "carousel", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Carousel", + "filter": "carousel" + } + } + } + } + }, + { + "title": "Carousel Card", + "id": "carousel-cards", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block/item", + "template": { + "name": "Carousel Cards", + "model": "carousel-cards" + } + } + } + } + } + ], + "models": [ + { + "id": "carousel", + "fields": [] + }, + { + "id": "carousel-cards", + "fields": [ + { + "component": "reference", + "valueType": "string", + "name": "image", + "label": "Image" + }, + { + "component": "select", + "valueType": "string", + "name": "alignment", + "label": "Card Alignment", + "value": "center", + "options": [ + { + "name": "Left", + "value": "left" + }, + { + "name": "Center", + "value": "center" + }, + { + "name": "Right", + "value": "right" + } + ] + }, + { + "component": "text", + "valueType": "string", + "name": "title", + "label": "Card Title" + }, + { + "component": "richtext", + "valueType": "string", + "name": "description", + "label": "Card Description" + }, + { + "component": "text", + "valueType": "string", + "name": "buttonLabel", + "label": "Button Label" + }, + { + "component": "aem-content", + "name": "buttonLink", + "label": "Button Link" + } + ] + } + ], + "filters": [ + { + "id": "carousel", + "components": [ + "carousel-cards" + ] + } + ] +} + diff --git a/blocks/carousel/carousel.css b/blocks/carousel/carousel.css new file mode 100644 index 0000000..73af487 --- /dev/null +++ b/blocks/carousel/carousel.css @@ -0,0 +1,261 @@ +/* ========================= + Base Carousel Layout +========================= */ + +.carousel { + position: relative; + overflow: hidden; +} + +.carousel-viewport { + overflow: hidden; + width: 100%; +} + +.carousel-track { + display: flex; + transition: transform 0.6s ease; +} + +.carousel-slide { + min-width: 100%; + position: relative; +} + +/* Image */ +.carousel-slide picture img { + width: 100%; + height: 500px; + object-fit: cover; + display: block; +} + +/* ========================= + Overlay Base (Auto Height) +========================= */ + +.carousel-overlay { + position: absolute; + bottom: 40px; + padding: 2.5rem 3rem; + width: 45%; + color: #fff; + z-index: 2; + border-radius: 6px; + background: rgba(0, 0, 0, 0.65); +} + +/* Gradient background for readability */ +.carousel-overlay::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient( + to right, + rgba(0, 0, 0, 0.75), + rgba(0, 0, 0, 0.4), + transparent + ); +} + +/* ========================= + Alignment Variants +========================= */ + +/* LEFT */ +.overlay-left { + left: 60px; + text-align: left; +} + +/* RIGHT */ +.overlay-right { + right: 60px; + text-align: left; +} + +/* CENTER */ +.overlay-center { + left: 50%; + transform: translateX(-50%); + text-align: center; + width: 60%; +} + +.overlay-right::before { + background: linear-gradient( + to left, + rgba(0, 0, 0, 0.75), + rgba(0, 0, 0, 0.4), + transparent + ); +} + +.overlay-center::before { + background: rgba(0, 0, 0, -0.45); +} + +/* ========================= + Overlay cells (preserve UE bindings) +========================= */ + +/* Hidden cells: keep in DOM for UE, hide visually */ +.carousel-overlay .carousel-alignment, +.carousel-overlay .carousel-button-style, +.carousel-overlay .carousel-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.carousel-overlay .carousel-title { + font-size: 2.5rem; + margin-bottom: 1rem; + line-height: 1.2; + font-weight: 700; +} + +.carousel-overlay .carousel-desc { + font-size: 1.1rem; + margin: 0.5rem 0; +} + +/* ========================= + Typography +========================= */ + +.carousel-overlay h1, +.carousel-overlay h2, +.carousel-overlay h3 { + font-size: 2.5rem; + margin-bottom: 1rem; + line-height: 1.2; +} + +.carousel-overlay p { + font-size: 1.1rem; + margin: 0.5rem 0; +} + +/* ========================= + Button Styling +========================= */ + +/* Higher specificity than styles.css a.button:any-link (0,2,1) */ +.carousel .carousel-overlay .carousel-button { + display: inline-block; + margin-top: 1.8rem; + padding: 0.85rem 2rem; + background-color: #C7C6C1; + color: #000 !important; + text-decoration: none !important; + font-weight: 600; + border-radius: 4px; + transition: all 0.3s ease; +} + +.carousel .carousel-overlay .carousel-button:hover { + background-color: #000; + color: #fff !important; + text-decoration: none !important; +} + +.carousel-overlay a { + text-decoration: none; +} + +/* ========================= + Navigation Arrows +========================= */ + +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.6); + border: none; + color: #fff; + padding: 1rem 1.2rem; + cursor: pointer; + z-index: 3; + font-size: 1.2rem; + border-radius: 4px; + transition: 0.3s ease; +} + +.carousel-nav.prev { + left: 20px; +} + +.carousel-nav.next { + right: 20px; +} + +.carousel-nav:hover { + background: rgba(0, 0, 0, 0.9); +} + +/* ========================= + Responsive +========================= */ + +@media (max-width: 992px) { + .carousel-slide picture img { + height: 450px; + } + + .carousel-overlay { + width: 70%; + padding: 3rem 2rem; + } + + .carousel-overlay h1, + .carousel-overlay h2, + .carousel-overlay h3 { + font-size: 2.2rem; + } +} + +@media (max-width: 768px) { + .carousel-slide picture img { + height: 380px; + } + + .carousel-overlay { + width: 90%; + left: 50% !important; + right: auto !important; + transform: translateX(-50%) !important; + bottom: 20px; + text-align: center; + } + + .overlay-left, + .overlay-right, + .overlay-center { + left: 0; + right: 0; + transform: none; + } + + .carousel-overlay::before { + background: rgba(0, 0, 0, 0.6); + } + + .carousel-overlay h1, + .carousel-overlay h2, + .carousel-overlay h3 { + font-size: 1.8rem; + } + + .carousel-overlay p { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/blocks/carousel/carousel.js b/blocks/carousel/carousel.js new file mode 100644 index 0000000..17d96d0 --- /dev/null +++ b/blocks/carousel/carousel.js @@ -0,0 +1,154 @@ +export default function decorate(block) { + const originalSlides = [...block.children]; + if (originalSlides.length <= 1) return; + + const viewport = document.createElement('div'); + viewport.className = 'carousel-viewport'; + + const track = document.createElement('div'); + track.className = 'carousel-track'; + + /* ---------------------------- + Prepare Slides (overlay logic) + ----------------------------- */ + + const getCellText = (el) => (el?.textContent?.trim() ?? ''); + const applyButtonClass = (linkCell, label) => { + const a = linkCell?.querySelector('a'); + if (a) { + a.classList.add('carousel-button'); + a.textContent = label || a.textContent || 'CTA Button'; + } + }; + + originalSlides.forEach((slide) => { + slide.classList.add('carousel-slide'); + + const cells = [...slide.children]; + const alignmentCell = cells[1]; + const titleCell = cells[2]; + const descCell = cells[3]; + const buttonLabelCell = cells[4]; + const buttonLinkCell = cells[5]; + const styleCell = cells[6]; + + const alignmentRaw = getCellText(alignmentCell).toLowerCase(); + let alignment = 'left'; + if (alignmentRaw === 'center') alignment = 'center'; + else if (alignmentRaw === 'right') alignment = 'right'; + + const title = getCellText(titleCell); + const description = getCellText(descCell); + const buttonLabel = getCellText(buttonLabelCell); + const hasLink = buttonLinkCell?.querySelector('a') || getCellText(buttonLinkCell); + const hasDetails = !!(title || description || buttonLabel || hasLink); + + if (hasDetails) { + const overlay = document.createElement('div'); + overlay.className = `carousel-overlay overlay-${alignment}`; + + [alignmentCell, titleCell, descCell, buttonLabelCell, buttonLinkCell, styleCell].forEach((c) => { + if (c?.parentNode) { + c.classList.add('carousel-overlay-cell'); + if (c === alignmentCell) c.classList.add('carousel-alignment'); + if (c === titleCell) c.classList.add('carousel-title'); + if (c === descCell) c.classList.add('carousel-desc'); + if (c === buttonLabelCell) c.classList.add('carousel-label'); + if (c === buttonLinkCell) { + c.classList.add('carousel-link'); + applyButtonClass(c, buttonLabel); + } + if (c === styleCell) c.classList.add('carousel-button-style'); + overlay.appendChild(c); + } + }); + slide.appendChild(overlay); + } else { + [alignmentCell, titleCell, descCell, buttonLabelCell, buttonLinkCell, styleCell].forEach((c) => { + if (c?.parentNode) c.remove(); + }); + } + }); + + /* ---------------------------- + Infinite Loop Setup + ----------------------------- */ + + const stripEditorAttrs = (el) => { + const nodes = [el, ...el.querySelectorAll('*')]; + nodes.forEach((node) => { + [...node.attributes].forEach((attr) => { + if (attr.name.startsWith('data-aue-') || attr.name.startsWith('data-richtext-')) { + node.removeAttribute(attr.name); + } + }); + }); + }; + + const firstClone = originalSlides[0].cloneNode(true); + const lastClone = originalSlides[originalSlides.length - 1].cloneNode(true); + + [firstClone, lastClone].forEach((clone) => { + clone.classList.add('clone'); + stripEditorAttrs(clone); + }); + + track.append(lastClone); + originalSlides.forEach((slide) => track.append(slide)); + track.append(firstClone); + + viewport.append(track); + block.textContent = ''; + block.append(viewport); + + /* ---------------------------- + Navigation + ----------------------------- */ + + const prevBtn = document.createElement('button'); + prevBtn.className = 'carousel-nav prev'; + prevBtn.innerHTML = '❮'; + + const nextBtn = document.createElement('button'); + nextBtn.className = 'carousel-nav next'; + nextBtn.innerHTML = '❯'; + + block.append(prevBtn, nextBtn); + + let currentIndex = 1; + const totalSlides = originalSlides.length; + + function setPosition(withTransition = true) { + track.style.transition = withTransition + ? 'transform 0.6s ease' + : 'none'; + + track.style.transform = `translateX(-${currentIndex * 100}%)`; + } + + setPosition(false); + + nextBtn.addEventListener('click', () => { + if (currentIndex >= totalSlides + 1) return; + currentIndex++; + setPosition(); + }); + + prevBtn.addEventListener('click', () => { + if (currentIndex <= 0) return; + currentIndex--; + setPosition(); + }); + + track.addEventListener('transitionend', () => { + if (currentIndex === totalSlides + 1) { + currentIndex = 1; + setPosition(false); + } + + if (currentIndex === 0) { + currentIndex = totalSlides; + setPosition(false); + } + }); +} \ No newline at end of file diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index f2b203e..f96f2b2 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -5,6 +5,7 @@ .columns img { width: 100%; + border-radius: 8px; } .columns > div > div { @@ -17,6 +18,7 @@ .columns > div > .columns-img-col img { display: block; + border-radius: 8px; } @media (width >= 900px) { @@ -31,3 +33,56 @@ order: unset; } } + +.columns a:any-link { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + max-width: 100%; + margin: 0; + border: 2px solid var(--text-color); + border-radius: 2.4em; + padding: 0.5em 1.2em; + font: inherit; + font-weight: 500; + line-height: 1.25; + text-decoration: none; + background-color: transparent; + color: var(--text-color); + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.columns a:hover, +.columns a:focus-visible { + border-color: var(--dark-color); + background-color: var(--light-color); + color: var(--dark-color); + text-decoration: none; +} + +footer .columns a:any-link, +footer .footer .columns a:any-link { + border: none; + border-radius: 0; + padding: 0; + margin: 0; + background-color: transparent; + font-weight: normal; + display: inline; + text-decoration: none; + color: var(--text-color); +} + +footer .columns a:hover, +footer .columns a:focus-visible, +footer .footer .columns a:hover, +footer .footer .columns a:focus-visible { + border: none; + background-color: transparent; + color: var(--text-color); + text-decoration: underline; +} diff --git a/blocks/contact-form/_contact-form.json b/blocks/contact-form/_contact-form.json new file mode 100644 index 0000000..2b426f0 --- /dev/null +++ b/blocks/contact-form/_contact-form.json @@ -0,0 +1,42 @@ +{ + "definitions": [ + { + "title": "Contact Form", + "id": "contact-form", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Contact Form", + "model": "contact-form" + } + } + } + } + } + ], + "models": [ + { + "id": "contact-form", + "fields": [ + { + "component": "text", + "valueType": "string", + "name": "endpoint", + "label": "Submission Endpoint URL", + "description": "HTTPS endpoint that receives JSON and writes to AEM or Google Sheet." + }, + { + "component": "text", + "valueType": "string", + "name": "successMessage", + "label": "Success Message", + "value": "Thank you. Your request has been submitted." + } + ] + } + ], + "filters": [] +} + diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css new file mode 100644 index 0000000..06cddcb --- /dev/null +++ b/blocks/contact-form/contact-form.css @@ -0,0 +1,152 @@ +.contact-form-wrapper { + max-width: 640px; + margin: 0 auto; +} + +.contact-form { + padding: 24px 24px 28px; + border-radius: 16px; + background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 100%); + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.contact-form-title { + margin-top: 0; + margin-bottom: 4px; +} + +.contact-form-description { + margin-top: 0; + margin-bottom: 16px; + font-size: var(--body-font-size-s); + color: var(--dark-color); +} + +.contact-form-field { + margin-top: 12px; + display: flex; + flex-direction: column; +} + +.contact-form-field label { + margin-bottom: 4px; + font-size: var(--body-font-size-xs); + font-weight: 500; +} + +.contact-form-field input[type='text'], +.contact-form-field input[type='email'], +.contact-form-field input[type='tel'], +.contact-form-field textarea { + border-radius: 8px; + border: 1px solid #d0d7e2; + padding: 8px 10px; + font-size: var(--body-font-size-xs); + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.contact-form-field input[type='text']:focus, +.contact-form-field input[type='email']:focus, +.contact-form-field input[type='tel']:focus, +.contact-form-field textarea:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(59, 99, 251, 0.1); +} + +.contact-form-field textarea { + resize: vertical; + min-height: 96px; +} + +.contact-form-error { + min-height: 16px; + margin-top: 4px; + font-size: 12px; + color: #c62828; +} + +.contact-form-terms { + flex-direction: row; + align-items: flex-start; + gap: 8px; +} + +.contact-form-terms input[type='checkbox'] { + margin-top: 4px; +} + +.contact-form-terms label { + margin: 0; + font-size: var(--body-font-size-xs); +} + +.contact-form-terms-link { + border: 0; + padding: 0; + margin: 0; + background: none; + color: var(--link-color); + text-decoration: underline; + cursor: pointer; + font: inherit; +} + +.contact-form-status { + margin-top: 8px; + font-size: 13px; +} + +.contact-form-status.success { + color: #2e7d32; +} + +.contact-form-status.error { + color: #c62828; +} + +.contact-form-submit { + margin-top: 16px; +} + +.contact-form-modal-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.contact-form-modal { + max-width: 480px; + width: calc(100% - 32px); + background-color: #ffffff; + border-radius: 12px; + padding: 20px 20px 16px; + box-shadow: + 0 14px 45px rgba(0, 0, 0, 0.25), + 0 10px 18px rgba(0, 0, 0, 0.22); +} + +.contact-form-modal-body { + font-size: var(--body-font-size-xs); +} + +.contact-form-modal-body ul { + padding-left: 20px; +} + +.contact-form-modal-close { + margin-top: 12px; +} + +@media (width >= 900px) { + .contact-form { + padding: 28px 32px 32px; + } +} + diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js new file mode 100644 index 0000000..a1a3901 --- /dev/null +++ b/blocks/contact-form/contact-form.js @@ -0,0 +1,286 @@ +function buildInput(labelText, id, type = 'text', required = false) { + const field = document.createElement('div'); + field.className = 'contact-form-field'; + + const label = document.createElement('label'); + label.setAttribute('for', id); + label.textContent = labelText; + + const input = document.createElement('input'); + input.id = id; + input.name = id; + input.type = type; + if (required) input.required = true; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + field.append(label, input, error); + return { field, input, error }; +} + +function buildTextArea(labelText, id, required = false) { + const field = document.createElement('div'); + field.className = 'contact-form-field'; + + const label = document.createElement('label'); + label.setAttribute('for', id); + label.textContent = labelText; + + const textarea = document.createElement('textarea'); + textarea.id = id; + textarea.name = id; + textarea.rows = 4; + if (required) textarea.required = true; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + field.append(label, textarea, error); + return { field, textarea, error }; +} + +function buildTermsField() { + const wrapper = document.createElement('div'); + wrapper.className = 'contact-form-field contact-form-terms'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = 'cf-terms'; + checkbox.name = 'cf-terms'; + checkbox.required = true; + + const label = document.createElement('label'); + label.setAttribute('for', 'cf-terms'); + label.innerHTML = 'I have read and agree to the .'; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + wrapper.append(checkbox, label, error); + return { wrapper, checkbox, error }; +} + +function buildModal() { + const overlay = document.createElement('div'); + overlay.className = 'contact-form-modal-overlay'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-labelledby', 'cf-terms-title'); + + const dialog = document.createElement('div'); + dialog.className = 'contact-form-modal'; + + const title = document.createElement('h3'); + title.id = 'cf-terms-title'; + title.textContent = 'Terms and Conditions'; + + const body = document.createElement('div'); + body.className = 'contact-form-modal-body'; + body.innerHTML = ` +

Please review the trek terms and conditions before submitting the form.

+ + `; + + const close = document.createElement('button'); + close.type = 'button'; + close.className = 'contact-form-modal-close'; + close.textContent = 'Close'; + + dialog.append(title, body, close); + overlay.append(dialog); + + close.addEventListener('click', () => { + overlay.remove(); + }); + + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + overlay.remove(); + } + }); + + return overlay; +} + +function getConfig(block) { + const rows = [...block.querySelectorAll(':scope > div')]; + const firstRowCell = rows[0]?.querySelector(':scope > div'); + const secondRowCell = rows[1]?.querySelector(':scope > div'); + + const endpoint = firstRowCell ? firstRowCell.textContent.trim() : ''; + const successMessage = secondRowCell ? secondRowCell.textContent.trim() : ''; + + return { + endpoint, + successMessage: successMessage || 'Thank you. Your request has been submitted.', + }; +} + +export default function decorate(block) { + const { endpoint, successMessage } = getConfig(block); + + block.innerHTML = ''; + + const formWrapper = document.createElement('div'); + formWrapper.className = 'contact-form-wrapper'; + + const form = document.createElement('form'); + form.className = 'contact-form'; + form.noValidate = true; + + const heading = document.createElement('h2'); + heading.className = 'contact-form-title'; + heading.textContent = 'Contact for Trek Enquiry'; + + const description = document.createElement('p'); + description.className = 'contact-form-description'; + description.textContent = 'Share your details and we will get back to you with trek information and availability.'; + + const nameField = buildInput('Name', 'cf-name', 'text', true); + const emailField = buildInput('Email', 'cf-email', 'email', true); + const phoneField = buildInput('Contact Number', 'cf-phone', 'tel', true); + const trekField = buildInput('Trek Name', 'cf-trek', 'text', true); + const commentsField = buildTextArea('Queries / Comments', 'cf-comments', true); + const termsField = buildTermsField(); + + const status = document.createElement('div'); + status.className = 'contact-form-status'; + status.setAttribute('aria-live', 'polite'); + + const submit = document.createElement('button'); + submit.type = 'submit'; + submit.className = 'contact-form-submit button'; + submit.textContent = 'Submit'; + + form.append( + heading, + description, + nameField.field, + emailField.field, + phoneField.field, + trekField.field, + commentsField.field, + termsField.wrapper, + submit, + status, + ); + + formWrapper.append(form); + block.append(formWrapper); + + // terms modal + const termsButton = termsField.wrapper.querySelector('.contact-form-terms-link'); + termsButton.addEventListener('click', () => { + const modal = buildModal(); + document.body.append(modal); + }); + + function clearErrors() { + [nameField, emailField, phoneField, trekField, commentsField, { error: termsField.error }].forEach((f) => { + if (f.error) f.error.textContent = ''; + }); + status.textContent = ''; + } + + function validate() { + clearErrors(); + let valid = true; + + if (!nameField.input.value.trim()) { + nameField.error.textContent = 'Please enter your name.'; + valid = false; + } + + const email = emailField.input.value.trim(); + if (!email) { + emailField.error.textContent = 'Please enter your email.'; + valid = false; + } else if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { + emailField.error.textContent = 'Please enter a valid email address.'; + valid = false; + } + + const phone = phoneField.input.value.trim(); + if (!phone) { + phoneField.error.textContent = 'Please enter your contact number.'; + valid = false; + } else if (!/^[0-9+\-\s()]{7,20}$/.test(phone)) { + phoneField.error.textContent = 'Please enter a valid contact number.'; + valid = false; + } + + if (!trekField.input.value.trim()) { + trekField.error.textContent = 'Please enter the trek name.'; + valid = false; + } + + if (!commentsField.textarea.value.trim()) { + commentsField.error.textContent = 'Please add your queries or comments.'; + valid = false; + } + + if (!termsField.checkbox.checked) { + termsField.error.textContent = 'You must agree to the terms and conditions.'; + valid = false; + } + + return valid; + } + + async function submitForm(event) { + event.preventDefault(); + if (!validate()) return; + + if (!endpoint) { + status.textContent = 'Form endpoint is not configured.'; + status.classList.remove('success'); + status.classList.add('error'); + return; + } + + submit.disabled = true; + status.textContent = 'Submitting...'; + status.classList.remove('success', 'error'); + + const payload = { + name: nameField.input.value.trim(), + email: emailField.input.value.trim(), + phone: phoneField.input.value.trim(), + trekName: trekField.input.value.trim(), + comments: commentsField.textarea.value.trim(), + timestamp: new Date().toISOString(), + }; + + try { + await fetch(endpoint, { + method: 'POST', + mode: 'no-cors', + body: JSON.stringify(payload), + }); + + // With no-cors the response is opaque; assume success if the + // network request did not fail at the browser level. + status.textContent = successMessage; + status.classList.remove('error'); + status.classList.add('success'); + form.reset(); + } catch (e) { + status.textContent = 'Something went wrong while submitting the form. Please try again.'; + status.classList.remove('success'); + status.classList.add('error'); + } finally { + submit.disabled = false; + } + } + + form.addEventListener('submit', submitForm); +} + diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index d8617de..db813ea 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,5 +1,5 @@ footer { - background-color: var(--light-color); + background-color: #d6ecff; font-size: var(--body-font-size-xs); } diff --git a/blocks/header/header.css b/blocks/header/header.css index 19d07bc..19bc63e 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,6 +1,6 @@ /* header and nav layout */ header .nav-wrapper { - background-color: var(--background-color); + background-color: #d6ecff; width: 100%; z-index: 2; position: fixed; @@ -72,7 +72,7 @@ header nav .nav-hamburger button { border: 0; border-radius: 0; padding: 0; - background-color: var(--background-color); + background-color: #d6ecff; color: inherit; overflow: initial; text-overflow: initial; @@ -149,8 +149,8 @@ header .nav-brand { } header nav .nav-brand img { - width: 128px; - height: auto; + width: 70px; + height: 65px; } /* sections */ diff --git a/blocks/logo-cards/_logo-cards.json b/blocks/logo-cards/_logo-cards.json new file mode 100644 index 0000000..7e77e0c --- /dev/null +++ b/blocks/logo-cards/_logo-cards.json @@ -0,0 +1,86 @@ +{ + "definitions": [ + { + "title": "Logo Cards", + "id": "logo-cards", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Logo Cards", + "filter": "logo-cards" + } + } + } + } + }, + { + "title": "Logo Card", + "id": "logo-card", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block/item", + "template": { + "name": "Logo Card", + "model": "logo-card" + } + } + } + } + } + ], + "models": [ + { + "id": "logo-cards", + "fields": [] + }, + { + "id": "logo-card", + "fields": [ + { + "component": "reference", + "valueType": "string", + "name": "icon", + "label": "Logo / Icon", + "multi": false + }, + { + "component": "text", + "valueType": "string", + "name": "title", + "label": "Title" + }, + { + "component": "richtext", + "valueType": "string", + "name": "description", + "label": "Description", + "value": "" + }, + { + "component": "text", + "valueType": "string", + "name": "cta-label", + "label": "CTA Label" + }, + { + "component": "text", + "valueType": "string", + "name": "cta-url", + "label": "CTA URL" + } + ] + } + ], + "filters": [ + { + "id": "logo-cards", + "components": [ + "logo-card" + ] + } + ] +} + diff --git a/blocks/logo-cards/logo-cards.css b/blocks/logo-cards/logo-cards.css new file mode 100644 index 0000000..56ce178 --- /dev/null +++ b/blocks/logo-cards/logo-cards.css @@ -0,0 +1,67 @@ +.logo-cards > ul { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 24px; +} + +.logo-cards > ul > li { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 20px 24px; + border: 1px solid #10b4a2; + background-color: var(--background-color, #fff); +} + +.logo-card-icon { + flex: 0 0 auto; + line-height: 0; +} + +.logo-card-icon picture, +.logo-card-icon img { + display: block; + width: 40px; + height: 40px; + object-fit: contain; +} + +.logo-card-content { + flex: 1 1 auto; +} + +.logo-card-title { + margin: 0 0 4px; + font-size: 1rem; + font-weight: 600; +} + +.logo-card-description { + margin: 0; + font-size: 0.9rem; + color: var(--text-color, #444); +} + +.logo-card-cta { + display: inline-block; + margin-top: 8px; + font-size: 0.9rem; + font-weight: 600; + color: var(--link-color, #10b4a2); + text-decoration: none; +} + +.logo-card-cta:hover, +.logo-card-cta:focus { + text-decoration: underline; +} + +@media (max-width: 600px) { + .logo-cards > ul > li { + padding: 16px 18px; + } +} + diff --git a/blocks/logo-cards/logo-cards.js b/blocks/logo-cards/logo-cards.js new file mode 100644 index 0000000..50662e5 --- /dev/null +++ b/blocks/logo-cards/logo-cards.js @@ -0,0 +1,82 @@ +import { createOptimizedPicture } from '../../scripts/aem.js'; +import { moveInstrumentation } from '../../scripts/scripts.js'; + +export default function decorate(block) { + const rows = [...block.querySelectorAll(':scope > div')]; + + const ul = document.createElement('ul'); + + rows.forEach((row) => { + const li = document.createElement('li'); + moveInstrumentation(row, li); + + const cells = [...row.children]; + + const iconCell = cells[0] || null; + const titleCell = cells[1] || null; + const descriptionCell = cells[2] || null; + const ctaCell = cells[3] || null; + + let iconWrapper = null; + if (iconCell) { + const rawImg = iconCell.querySelector('picture > img, img'); + if (rawImg && rawImg.src) { + const optimized = createOptimizedPicture( + rawImg.src, + rawImg.alt || '', + false, + [{ width: '64' }], + ); + const optimizedImg = optimized.querySelector('img'); + if (optimizedImg) { + moveInstrumentation(rawImg, optimizedImg); + } + const pictureOrImg = rawImg.closest('picture') || rawImg; + pictureOrImg.replaceWith(optimized); + + iconWrapper = document.createElement('div'); + iconWrapper.className = 'logo-card-icon'; + iconWrapper.append(optimized); + } + } + + const content = document.createElement('div'); + content.className = 'logo-card-content'; + + const titleText = titleCell ? titleCell.textContent.trim() : ''; + if (titleText) { + const titleEl = document.createElement('h3'); + titleEl.className = 'logo-card-title'; + titleEl.textContent = titleText; + content.appendChild(titleEl); + } + + const descriptionText = descriptionCell ? descriptionCell.textContent.trim() : ''; + if (descriptionText) { + const descEl = document.createElement('p'); + descEl.className = 'logo-card-description'; + descEl.textContent = descriptionText; + content.appendChild(descEl); + } + + if (ctaCell) { + const link = ctaCell.querySelector('a'); + if (link && link.href) { + const ctaEl = document.createElement('a'); + ctaEl.className = 'logo-card-cta'; + ctaEl.href = link.href; + ctaEl.textContent = (link.textContent || '').trim() || link.href; + content.appendChild(ctaEl); + } + } + + if (iconWrapper) { + li.appendChild(iconWrapper); + } + + li.appendChild(content); + ul.appendChild(li); + }); + + block.replaceChildren(ul); +} diff --git a/component-definition.json b/component-definition.json index 0ee8f70..343f7f4 100644 --- a/component-definition.json +++ b/component-definition.json @@ -82,6 +82,21 @@ "title": "Blocks", "id": "blocks", "components": [ + { + "title": "Contact Form", + "id": "contact-form", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Contact Form", + "model": "contact-form" + } + } + } + } + }, { "title": "Cards", "id": "cards", @@ -97,6 +112,36 @@ } } }, + { + "title": "Logo Cards", + "id": "logo-cards", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Logo Cards", + "filter": "logo-cards" + } + } + } + } + }, + { + "title": "Logo Card", + "id": "logo-card", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block/item", + "template": { + "name": "Logo Card", + "model": "logo-card" + } + } + } + } + }, { "title": "Card", "id": "card", @@ -271,7 +316,22 @@ "resourceType": "core/franklin/components/block/v1/block", "template": { "name": "Carousel", - "model": "carousel" + "filter": "carousel" + } + } + } + } + }, + { + "title": "Carousel Cards", + "id": "carousel-cards", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block/item", + "template": { + "name": "Carousel Cards", + "model": "carousel-cards" } } } diff --git a/component-filters.json b/component-filters.json index ca1c484..4b0ddc3 100644 --- a/component-filters.json +++ b/component-filters.json @@ -14,6 +14,8 @@ "title", "hero", "cards", + "contact-form", + "logo-cards", "columns", "fragment", "weather", @@ -31,6 +33,18 @@ "card" ] }, + { + "id": "logo-cards", + "components": [ + "logo-card" + ] + }, + { + "id": "carousel", + "components": [ + "carousel-cards" + ] + }, { "id": "columns", "components": [ diff --git a/component-models.json b/component-models.json index 06f5a87..a1b05db 100644 --- a/component-models.json +++ b/component-models.json @@ -272,6 +272,10 @@ { "name": "Madurai", "value": "Madurai,IN" + }, + { + "name": "Nepal", + "value": "Nepal" } ] } @@ -400,5 +404,120 @@ { "id": "carousel", "fields": [] + }, + { + "id": "carousel-cards", + "fields": [ + { + "component": "reference", + "valueType": "string", + "name": "image", + "label": "Image" + }, + { + "component": "select", + "valueType": "string", + "name": "alignment", + "label": "Card Alignment", + "value": "center", + "options": [ + { + "name": "Left", + "value": "left" + }, + { + "name": "Center", + "value": "center" + }, + { + "name": "Right", + "value": "right" + } + ] + }, + { + "component": "text", + "valueType": "string", + "name": "title", + "label": "Card Title" + }, + { + "component": "richtext", + "valueType": "string", + "name": "description", + "label": "Card Description" + }, + { + "component": "text", + "valueType": "string", + "name": "buttonLabel", + "label": "Button Label" + }, + { + "component": "aem-content", + "name": "buttonLink", + "label": "Button Link" + } + ] + }, + { + "id": "contact-form", + "fields": [ + { + "component": "text", + "valueType": "string", + "name": "endpoint", + "label": "Submission Endpoint URL", + "description": "HTTPS endpoint that receives JSON and writes to AEM or Google Sheet." + }, + { + "component": "text", + "valueType": "string", + "name": "successMessage", + "label": "Success Message", + "value": "Thank you. Your request has been submitted." + } + ] + }, + { + "id": "logo-cards", + "fields": [] + }, + { + "id": "logo-card", + "fields": [ + { + "component": "reference", + "valueType": "string", + "name": "icon", + "label": "Logo / Icon", + "multi": false + }, + { + "component": "text", + "valueType": "string", + "name": "title", + "label": "Title" + }, + { + "component": "richtext", + "valueType": "string", + "name": "description", + "label": "Description", + "value": "" + }, + { + "component": "text", + "valueType": "string", + "name": "cta-label", + "label": "CTA Label" + }, + { + "component": "text", + "valueType": "string", + "name": "cta-url", + "label": "CTA URL" + } + ] } -] \ No newline at end of file +] diff --git a/models/_section.json b/models/_section.json index 0cee550..c9fe067 100644 --- a/models/_section.json +++ b/models/_section.json @@ -49,6 +49,7 @@ "title", "hero", "cards", + "logo-cards", "columns", "fragment" ]