fix: add newsletter box

This commit is contained in:
Tim Rijkse
2026-01-15 15:28:14 +01:00
parent 968d2036b4
commit 1a6b47c8d3
9 changed files with 313 additions and 8 deletions

View File

@@ -17,6 +17,7 @@ import "./components/section-title.js";
import "./components/add-to-cart-button.js";
import "./components/cta-button.js";
import "./components/category-card.js";
import "./components/newsletter-signup.js";
// App initialization (if needed)
document.addEventListener("DOMContentLoaded", () => {

View File

@@ -47,15 +47,14 @@ class CategoryCard extends HTMLElement {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
display: flex;
}
.card {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
flex: 1;
color: inherit;
padding: var(--spacing-md, 0.875rem) var(--spacing-xs, 0.25rem);
text-decoration: none;

View File

@@ -0,0 +1,193 @@
/**
* Newsletter Signup Component
* A newsletter subscription form with title, description, and email input
*/
import { sendIcon } from "../icons/send-icon.js";
class NewsletterSignup extends HTMLElement {
static get observedAttributes() {
return ["title", "description", "button-text", "placeholder"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
attributeChangedCallback() {
this.render();
this.setupEventListeners();
}
get title() {
return this.getAttribute("title") || "Blijf op de hoogte";
}
get description() {
return (
this.getAttribute("description") ||
"Schrijf je in voor onze nieuwsbrief en ontvang het laatste nieuws over nieuwe boeken en aanbiedingen."
);
}
get buttonText() {
return this.getAttribute("button-text") || "Inschrijven";
}
get placeholder() {
return this.getAttribute("placeholder") || "Je e-mailadres";
}
setupEventListeners() {
const form = this.shadowRoot?.querySelector("form");
if (form) {
form.addEventListener("submit", (e) => {
e.preventDefault();
const emailInput = this.shadowRoot.querySelector('input[type="email"]');
const email = emailInput?.value;
if (email) {
// Dispatch custom event for parent components to handle
this.dispatchEvent(
new CustomEvent("newsletter-submit", {
detail: { email },
bubbles: true,
composed: true,
})
);
// Reset form
form.reset();
// Show feedback (you could enhance this)
console.log("Newsletter signup:", email);
}
});
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.newsletter-box {
border: 1px solid var(--color-newsletter-border, #951D51);
border-radius: 4px;
padding: 24px 16px;
background: transparent;
}
.newsletter-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.newsletter-title {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 24px;
font-weight: 700;
line-height: 34px;
color: var(--color-newsletter-title, #951D51);
margin: 0;
}
.newsletter-description {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 32px;
color: #000000;
margin: 0;
}
.newsletter-form {
display: flex;
gap: 16px;
}
.newsletter-input {
flex: 1 1 auto;
min-width: 0;
padding: 12px 16px;
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
line-height: 24px;
border: 1px solid var(--color-newsletter-border, #951D51);
border-radius: 4px;
background-color: #ffffff;
outline: none;
transition: border-color 150ms ease;
}
.newsletter-input::placeholder {
color: #9CA3AF;
}
.newsletter-input:focus {
border-color: var(--color-newsletter-border, #951D51);
box-shadow: 0 0 0 1px var(--color-newsletter-border, #951D51);
}
.newsletter-button {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
height: 48px;
padding: 0;
color: #ffffff;
background-color: var(--color-newsletter-button, #951D51);
border: 1px solid var(--color-newsletter-button, #951D51);
border-radius: 4px;
cursor: pointer;
transition: background-color 150ms ease, border-color 150ms ease;
}
.newsletter-button svg {
width: 24px;
height: 24px;
}
.newsletter-button:hover {
background-color: var(--color-newsletter-button-hover, #7a1842);
border-color: var(--color-newsletter-button-hover, #7a1842);
}
.newsletter-button:focus {
outline: 2px solid var(--color-newsletter-button, #951D51);
outline-offset: 2px;
}
</style>
<div class="newsletter-box">
<div class="newsletter-content">
<h2 class="newsletter-title">${this.title}</h2>
<p class="newsletter-description">${this.description}</p>
<form class="newsletter-form">
<input
type="email"
class="newsletter-input"
placeholder="${this.placeholder}"
required
aria-label="E-mailadres"
/>
<button type="submit" class="newsletter-button" aria-label="${this.buttonText || 'Inschrijven'}">
${sendIcon({ size: 24, color: "#ffffff" })}
</button>
</form>
</div>
</div>
`;
}
}
customElements.define("newsletter-signup", NewsletterSignup);

View File

@@ -15,10 +15,37 @@ class PushBox extends HTMLElement {
connectedCallback() {
this.render();
// Use requestAnimationFrame to ensure slots are assigned
requestAnimationFrame(() => {
this.updateLogoVisibility();
});
}
attributeChangedCallback() {
this.render();
requestAnimationFrame(() => {
this.updateLogoVisibility();
});
}
updateLogoVisibility() {
const logoSlot = this.shadowRoot?.querySelector('slot[name="logo"]');
const logoWrapper = this.shadowRoot?.querySelector(".logo-wrapper");
if (logoSlot && logoWrapper) {
const assignedNodes = logoSlot.assignedNodes();
const hasContent =
assignedNodes.length > 0 &&
assignedNodes.some((node) => {
return node.nodeType === Node.ELEMENT_NODE && node.tagName === "IMG";
});
if (!hasContent) {
logoWrapper.classList.add("hidden");
} else {
logoWrapper.classList.remove("hidden");
}
}
}
get backgroundColor() {
@@ -53,6 +80,10 @@ class PushBox extends HTMLElement {
margin-bottom: 32px;
}
.logo-wrapper.hidden {
display: none;
}
.logo-wrapper ::slotted(img) {
width: 104px;
height: auto;
@@ -73,6 +104,12 @@ class PushBox extends HTMLElement {
font-size: 16px;
font-weight: 400;
}
.cta-wrapper ::slotted(.cta-buttons) {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
</style>
<div class="push-box">
<div class="logo-wrapper">

View File

@@ -1,6 +1,7 @@
/**
* Site Content Component
* Main content area wrapper with slot for page content
* Uses flexbox with gap for consistent vertical spacing
*/
class SiteContent extends HTMLElement {
constructor() {
@@ -22,8 +23,22 @@ class SiteContent extends HTMLElement {
}
.content {
padding-top: var(--spacing-md);
padding-bottom: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-md, 16px);
padding-top: var(--spacing-md, 16px);
padding-bottom: var(--spacing-md, 16px);
}
/* Remove vertical padding from direct children to let gap handle spacing */
::slotted(.content-padding) {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
::slotted(.section) {
padding-top: var(--spacing-md, 16px);
padding-bottom: var(--spacing-md, 16px);
}
</style>
<main class="content">

View File

@@ -7,3 +7,4 @@ export { searchIcon } from "./search.js";
export { menuIcon } from "./menu.js";
export { userIcon } from "./user.js";
export { shoppingBagIcon } from "./shopping-bag.js";
export { sendIcon } from "./send-icon.js";

31
js/icons/send-icon.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Send Icon (Lucide)
* @param {Object} props - Icon properties
* @param {number} props.size - Icon size (default: 24)
* @param {string} props.color - Icon color (default: currentColor)
* @param {number} props.strokeWidth - Stroke width (default: 2)
* @returns {string} SVG string
*/
export function sendIcon({
size = 24,
color = "currentColor",
strokeWidth = 2,
} = {}) {
return `
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
fill="none"
stroke="${color}"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-send-icon lucide-send"
>
<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/>
<path d="m21.854 2.147-10.94 10.939"/>
</svg>
`;
}