Skip to content

Instantly share code, notes, and snippets.

@thinkphp
Created July 27, 2025 15:48
Show Gist options
  • Save thinkphp/4756c1623125e91e68bb3670824eb0d9 to your computer and use it in GitHub Desktop.
Save thinkphp/4756c1623125e91e68bb3670824eb0d9 to your computer and use it in GitHub Desktop.
newsletter js
class SimpleNewsletter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Get attributes or set defaults
this.title = this.getAttribute('title') || 'Newsletter';
this.description = this.getAttribute('description') || 'Abonează-te pentru noutăți!';
this.apiUrl = this.getAttribute('api-url') || 'newsletter.php';
this.variant = this.getAttribute('variant') || 'default'; // default, compact, footer
this.placeholder = this.getAttribute('placeholder') || 'Adresa ta de email';
this.buttonText = this.getAttribute('button-text') || 'Abonează-te';
this.render();
this.attachEventListeners();
}
render() {
const variantStyles = this.getVariantStyles();
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.newsletter-container {
${variantStyles.container}
}
.newsletter-container:hover {
transform: translateY(-2px);
}
.newsletter-header {
text-align: center;
margin-bottom: 25px;
}
.newsletter-title {
${variantStyles.title}
margin-bottom: 10px;
font-weight: 700;
}
.newsletter-description {
${variantStyles.description}
font-size: 1rem;
line-height: 1.5;
}
.newsletter-form {
${variantStyles.form}
}
.newsletter-input {
${variantStyles.input}
}
.newsletter-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 15px rgba(102, 126, 234, 0.2);
}
.newsletter-button {
${variantStyles.button}
}
.newsletter-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.newsletter-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.loading {
display: none;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.message {
margin-top: 15px;
padding: 12px;
border-radius: 8px;
font-weight: 500;
text-align: center;
display: none;
animation: slideIn 0.3s ease;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.debug {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
font-size: 0.85rem;
text-align: left;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 768px) {
.newsletter-form {
flex-direction: column;
gap: 15px;
}
.newsletter-form.horizontal {
flex-direction: column;
}
.newsletter-input {
width: 100% !important;
flex: none !important;
}
.newsletter-button {
width: 100% !important;
}
}
</style>
<div class="newsletter-container">
<div class="newsletter-header">
<h3 class="newsletter-title">${this.title}</h3>
<p class="newsletter-description">${this.description}</p>
</div>
<form class="newsletter-form" id="newsletterForm">
<input
type="email"
class="newsletter-input"
id="emailInput"
placeholder="${this.placeholder}"
required>
<button type="submit" class="newsletter-button" id="submitButton">
<div class="loading" id="loading"></div>
<span id="buttonText">${this.buttonText}</span>
</button>
</form>
<div class="message" id="message"></div>
</div>
`;
}
getVariantStyles() {
switch (this.variant) {
case 'compact':
return {
container: `
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 12px;
text-align: center;
transition: all 0.3s ease;
`,
title: `
color: white;
font-size: 1.3rem;
`,
description: `
color: rgba(255, 255, 255, 0.9);
`,
form: `
display: flex;
gap: 10px;
max-width: 350px;
margin: 0 auto;
`,
input: `
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 25px;
font-size: 0.9rem;
background: rgba(255, 255, 255, 0.9);
`,
button: `
padding: 12px 20px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid white;
border-radius: 25px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
`
};
case 'footer':
return {
container: `
background: #2c3e50;
color: white;
padding: 30px;
border-radius: 12px;
text-align: center;
transition: all 0.3s ease;
`,
title: `
color: #ecf0f1;
font-size: 1.5rem;
`,
description: `
color: #bdc3c7;
`,
form: `
display: flex;
gap: 10px;
max-width: 400px;
margin: 0 auto;
`,
input: `
flex: 1;
padding: 15px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 25px;
font-size: 1rem;
background: rgba(255, 255, 255, 0.1);
color: white;
`,
button: `
padding: 15px 25px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid white;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
`
};
default:
return {
container: `
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
text-align: center;
transition: all 0.3s ease;
`,
title: `
color: #2c3e50;
font-size: 1.8rem;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
`,
description: `
color: #7f8c8d;
`,
form: `
display: flex;
max-width: 400px;
margin: 0 auto;
gap: 10px;
background: white;
padding: 8px;
border-radius: 50px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
`,
input: `
flex: 1;
border: none;
padding: 15px 20px;
font-size: 1rem;
border-radius: 50px;
background: transparent;
`,
button: `
padding: 15px 25px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
`
};
}
}
attachEventListeners() {
const form = this.shadowRoot.getElementById('newsletterForm');
form.addEventListener('submit', (e) => this.handleSubmit(e));
}
async handleSubmit(event) {
event.preventDefault();
const emailInput = this.shadowRoot.getElementById('emailInput');
const submitButton = this.shadowRoot.getElementById('submitButton');
const loading = this.shadowRoot.getElementById('loading');
const buttonText = this.shadowRoot.getElementById('buttonText');
const messageEl = this.shadowRoot.getElementById('message');
const email = emailInput.value.trim();
// Validate email
if (!this.isValidEmail(email)) {
this.showMessage(messageEl, 'Te rugăm să introduci o adresă de email validă.', 'error');
return;
}
// Show loading state
submitButton.disabled = true;
loading.style.display = 'inline-block';
buttonText.textContent = 'Se abonează...';
messageEl.style.display = 'none';
try {
console.log('Sending request to:', this.apiUrl);
console.log('Request data:', { email: email });
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ email: email })
});
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
// Get response text first
const responseText = await response.text();
console.log('Response text:', responseText);
// Check if response is empty
if (!responseText) {
throw new Error('Server-ul a returnat un răspuns gol');
}
// Try to parse JSON, but handle cases where it's not JSON
let result;
try {
result = JSON.parse(responseText);
} catch (jsonError) {
console.error('JSON parse error:', jsonError);
// If it's an HTML error page, show a debug message
if (responseText.includes('<html>') || responseText.includes('<!DOCTYPE')) {
this.showMessage(messageEl,
`Eroare server (${response.status}). Verifică că fișierul PHP funcționează corect. ` +
`<details style="margin-top: 10px;"><summary>Detalii tehnice</summary><pre style="font-size: 0.8em; margin-top: 5px;">${responseText.substring(0, 500)}...</pre></details>`,
'debug'
);
return;
} else {
throw new Error(`Răspuns invalid de la server: ${responseText.substring(0, 100)}...`);
}
}
// Handle the JSON response
if (result.success) {
this.showMessage(messageEl, result.message || 'Te-ai abonat cu succes!', 'success');
emailInput.value = '';
// Dispatch custom event for parent components
this.dispatchEvent(new CustomEvent('newsletter-subscribed', {
detail: { email: email, subscriberId: result.subscriber_id },
bubbles: true
}));
} else {
// Show debug info if available
let errorMessage = result.message || 'A apărut o eroare necunoscută.';
if (result.debug_info) {
errorMessage += `\n\nInfo debug:\n${JSON.stringify(result.debug_info, null, 2)}`;
this.showMessage(messageEl, errorMessage, 'debug');
} else {
this.showMessage(messageEl, errorMessage, 'error');
}
}
} catch (error) {
console.error('Newsletter subscription error:', error);
let errorMessage = 'Eroare de conexiune. ';
if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage += 'Nu se poate conecta la server. Verifică URL-ul API-ului.';
} else if (error.message.includes('JSON')) {
errorMessage += 'Răspuns invalid de la server.';
} else {
errorMessage += error.message;
}
this.showMessage(messageEl, errorMessage + ' Încearcă din nou.', 'error');
} finally {
// Reset button state
submitButton.disabled = false;
loading.style.display = 'none';
buttonText.textContent = this.buttonText;
}
}
showMessage(messageEl, text, type) {
// Handle HTML content for debug messages
if (type === 'debug') {
messageEl.innerHTML = text;
} else {
messageEl.textContent = text;
}
messageEl.className = `message ${type}`;
messageEl.style.display = 'block';
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
messageEl.style.display = 'none';
}, 5000);
}
}
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
// Observed attributes for dynamic updates
static get observedAttributes() {
return ['title', 'description', 'api-url', 'variant', 'placeholder', 'button-text'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
switch(name) {
case 'title':
this.title = newValue;
break;
case 'description':
this.description = newValue;
break;
case 'api-url':
this.apiUrl = newValue;
break;
case 'variant':
this.variant = newValue;
break;
case 'placeholder':
this.placeholder = newValue;
break;
case 'button-text':
this.buttonText = newValue;
break;
}
this.render();
this.attachEventListeners();
}
}
}
// Register the custom element
customElements.define('simple-newsletter', SimpleNewsletter);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment