Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/theme/Footer/Layout/enhanced-footer.css
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,12 @@ html[data-theme='light'] .enhanced-footer {
margin-bottom: 16px;
}

.newsletter-input-wrapper {
position: relative;
display: flex;
align-items: center;
}

.newsletter-input {
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
Expand All @@ -974,6 +980,29 @@ html[data-theme='light'] .enhanced-footer {
font-weight: 400;
backdrop-filter: blur(20px);
transition: all 0.3s ease;
width: 100%;
}

/* Enhanced specificity for input error state */
.enhanced-footer .newsletter-input.error,
[data-theme='dark'] .enhanced-footer .newsletter-input.error,
[data-theme='light'] .enhanced-footer .newsletter-input.error {
border-color: #ef4444 !important;
background: rgba(239, 68, 68, 0.15) !important;
color: #fca5a5 !important;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.3) !important;
}

.enhanced-footer .newsletter-input.error::placeholder,
[data-theme='dark'] .enhanced-footer .newsletter-input.error::placeholder,
[data-theme='light'] .enhanced-footer .newsletter-input.error::placeholder {
color: #fca5a5 !important;
opacity: 0.8;
}

.newsletter-input.validating {
border-color: #f59e0b !important;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3) !important;
}

.newsletter-input:focus {
Expand All @@ -987,6 +1016,82 @@ html[data-theme='light'] .enhanced-footer {
color: #94a3b8;
}

.validation-spinner {
position: absolute;
right: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
}

.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top: 2px solid #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

/* Enhanced specificity for error message to override footer protection */
.enhanced-footer .error-message,
[data-theme='dark'] .enhanced-footer .error-message,
[data-theme='light'] .enhanced-footer .error-message {
color: #ef4444 !important;
font-size: 13px !important;
font-weight: 600 !important;
margin-top: 4px !important;
padding: 8px 12px !important;
background: rgba(239, 68, 68, 0.2) !important;
border-radius: 8px !important;
border: 1px solid rgba(239, 68, 68, 0.5) !important;
animation: fadeIn 0.3s ease !important;
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.1) !important;
text-align: left !important;
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}

.newsletter-button {
padding: 14px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 8px 24px rgba(102, 126, 234, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.newsletter-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow:
0 4px 12px rgba(102, 126, 234, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

.newsletter-button {
padding: 14px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
Expand Down
128 changes: 113 additions & 15 deletions src/theme/Footer/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default function FooterLayout({
});
const [email, setEmail] = useState('');
const [isSubscribed, setIsSubscribed] = useState(false);
const [emailError, setEmailError] = useState<string | null>(null);
const [isValidating, setIsValidating] = useState(false);

useEffect(() => {
// Simulate real-time stats updates
Expand Down Expand Up @@ -55,15 +57,99 @@ export default function FooterLayout({
return () => clearInterval(interval);
}, []);

// Advanced email validation function
const validateEmail = (email: string): string | null => {
// Check basic format
const basicRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!basicRegex.test(email)) {
return 'Please enter a valid email address.';
}

// Check for common disposable email patterns
const disposableDomains = [
'10minutemail.com', 'tempmail.org', 'guerrillamail.com',
'mailinator.com', 'throwaway.email', 'disposablemail.com',
'sharklasers.com', 'trashmail.com', 'yopmail.com'
];

const domain = email.split('@')[1].toLowerCase();
if (disposableDomains.includes(domain)) {
return 'Disposable email addresses are not allowed.';
}

// Check for common free email domains with additional validation
const freeEmailDomains = [
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
'protonmail.com', 'icloud.com', 'aol.com', 'live.com'
];

if (!freeEmailDomains.includes(domain) && domain.length < 3) {
return 'Email domain appears to be invalid.';
}

// Check for common patterns that indicate invalid emails
if (email.length > 254) {
return 'Email address is too long.';
}

if (email.startsWith('.') || email.endsWith('.')) {
return 'Email address cannot start or end with a dot.';
}

if (email.includes('..')) {
return 'Email address cannot contain consecutive dots.';
}

// Check for valid characters in local part
const localPart = email.split('@')[0];
if (localPart.length > 64) {
return 'Email local part is too long.';
}

const validLocalRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
if (!validLocalRegex.test(localPart)) {
return 'Email contains invalid characters.';
}

return null; // Valid email
};

// Handle real-time validation
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);

// Clear error if field is empty
if (value === '') {
setEmailError(null);
return;
}

// Perform validation with a slight delay to avoid excessive checks
setIsValidating(true);
setTimeout(() => {
const error = validateEmail(value);
setEmailError(error);
setIsValidating(false);
}, 300);
};

const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault();
if (email) {
setIsSubscribed(true);
setTimeout(() => {
setIsSubscribed(false);
setEmail('');
}, 3000);

// Final validation before submission
const error = validateEmail(email);
if (error) {
setEmailError(error);
return;
}

setIsSubscribed(true);
setTimeout(() => {
setIsSubscribed(false);
setEmail('');
setEmailError(null); // Clear error on successful subscription
}, 3000);
};
return (
<footer className="enhanced-footer">
Expand Down Expand Up @@ -284,18 +370,30 @@ export default function FooterLayout({
Join {stats.activeUsers} developers getting weekly insights, tutorials, and exclusive content.
</p>
<form className="newsletter-form" onSubmit={handleSubscribe}>
<input
type="email"
placeholder="[email protected]"
className="newsletter-input"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<div className="newsletter-input-wrapper">
<input
type="email"
placeholder="[email protected]"
className={`newsletter-input ${emailError ? 'error' : ''} ${isValidating ? 'validating' : ''}`}
value={email}
onChange={handleEmailChange}
required
/>
{isValidating && (
<div className="validation-spinner">
<div className="spinner"></div>
</div>
)}
</div>
{emailError && (
<div className="error-message">
{emailError}
</div>
)}
<button
type="submit"
className={`newsletter-button ${isSubscribed ? 'subscribed' : ''}`}
disabled={isSubscribed}
disabled={isSubscribed || !!emailError || email === ''}
>
{isSubscribed ? '✓ Subscribed!' : 'Subscribe Now →'}
</button>
Expand Down