Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
101 changes: 101 additions & 0 deletions fern/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,105 @@ html.dark button[data-highlighted] .fern-api-property-meta {
/* Fix: Make subtitle white on Simulations pages in dark mode */
:is(.dark) [id*="simulations"] .prose-p\:text-\(color\:--grayscale-a11\) :where(p):not(:where([class~=not-prose],[class~=not-prose] *)) {
color: var(--grayscale-12) !important;
}

/* Subscribe form on What's New page */
.subscribe-form-row {
display: flex;
gap: 0.5rem;
}

.subscribe-form-input {
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
width: 100%;
font-size: 0.875rem;
outline: none;
transition: border-color 0.2s ease-in-out;
color: #1f2937;
background-color: #fff;
}

.subscribe-form-input:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 1px #4f46e5;
}

.subscribe-form-button {
background-color: #37aa9d;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
border: none;
cursor: pointer;
transition: all 0.2s ease-in-out;
white-space: nowrap;
}

.subscribe-form-button:hover {
background-color: #2e8b7d;
transform: translateY(-1px);
}

.subscribe-form-button:active {
transform: translateY(0);
}

.subscribe-form-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}

.subscribe-form-message {
margin-top: 0.5rem;
font-size: 0.875rem;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
}

.subscribe-form-message.success {
color: #065f46;
background-color: #d1fae5;
}

.subscribe-form-message.error {
color: #991b1b;
background-color: #fee2e2;
}

:is(.dark) .subscribe-form-input {
background-color: #374151;
border-color: #4b5563;
color: #f3f4f6;
}

:is(.dark) .subscribe-form-input::placeholder {
color: #9ca3af;
}

:is(.dark) .subscribe-form-input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 1px #6366f1;
}

:is(.dark) .subscribe-form-button {
background-color: #94ffd2;
color: #1f2937;
}

:is(.dark) .subscribe-form-button:hover {
background-color: #7cd9b0;
}

:is(.dark) .subscribe-form-message.success {
color: #a7f3d0;
background-color: #064e3b;
}

:is(.dark) .subscribe-form-message.error {
color: #fca5a5;
background-color: #7f1d1d;
}
66 changes: 5 additions & 61 deletions fern/changelog/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
slug: whats-new
---
<Card
title={<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', color: 'inherit' }} onClick={() => document.querySelector('input[type="email"]').focus()}>Subscribe to the latest product updates</div>}
title="Subscribe to the latest product updates"
icon="envelope"
iconType="solid"
>
Expand All @@ -11,80 +11,24 @@ slug: whats-new
action="https://customerioforms.com/forms/submit_action?site_id=5f95a74ff6539f0bc48f&form_id=01jk7tf2khhf5satn62531qe25&success_url=https://docs.vapi.ai/whats-new"
className="subscribe-form"
style={{margin: '1rem 0'}}
onSubmit={(e) => {
const emailInput = document.getElementById('email_input');
const emailValue = emailInput.value;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailValue)) {
e.preventDefault();
alert('Please enter a valid email address.');
}
}}
>
<div className="flex gap-2">
<div className="subscribe-form-row">
<label htmlFor="email_input" style={{ display: 'none' }}>E-mail address</label>
<input
id="email_input"
className="subscribe-form-input"
type="email"
name="email"
placeholder="Enter your email"
required
style={{
border: '1px solid #e2e8f0',
borderRadius: '0.375rem',
padding: '0.5rem 1rem',
width: '100%',
fontSize: '0.875rem',
outline: 'none',
transition: 'border-color 0.2s ease-in-out',
color: '#1f2937',
':focus': {
borderColor: '#4f46e5',
boxShadow: '0 0 0 1px #4f46e5'
},
'@media (prefers-color-scheme: dark)': {
backgroundColor: '#374151',
borderColor: '#4b5563',
color: '#f3f4f6',
'::placeholder': {
color: '#9ca3af'
},
':focus': {
borderColor: '#6366f1',
boxShadow: '0 0 0 1px #6366f1'
}
}
}}
/>
<button
type="submit"
style={{
backgroundColor: '#37aa9d',
color: 'white',
fontWeight: 500,
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
':hover': {
backgroundColor: '#2e8b7d',
transform: 'translateY(-1px)'
},
':active': {
transform: 'translateY(0)'
},
'@media (prefers-color-scheme: dark)': {
backgroundColor: '#94ffd2',
color: '#1f2937',
':hover': {
backgroundColor: '#7cd9b0'
}
}
}}
className="subscribe-form-button"
>
Submit
</button>
</div>
<div className="subscribe-form-message" style={{ display: 'none' }}></div>
</form>
</Card>
102 changes: 100 additions & 2 deletions fern/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,116 @@ function initializeHubSpot() {
document.getElementsByTagName('head')[0].appendChild(hubSpotScript);
}

function initializeSubscribeForm() {
// Fern's MDX renderer strips JSX event handlers (onSubmit, onClick), so the
// form's validation and submission logic must be attached from plain JS.
// Without this, the form falls through to a native HTML POST that silently
// redirects back to the same page with no user feedback.

var form = document.querySelector('form.subscribe-form');
if (!form) {
return;
}

// Avoid attaching the handler twice on SPA navigations
if (form.dataset.enhanced === 'true') {
return;
}
form.dataset.enhanced = 'true';

form.addEventListener('submit', function (e) {
e.preventDefault();

var emailInput = form.querySelector('input[name="email"]');
var submitBtn = form.querySelector('button[type="submit"]');
var messageDiv = form.querySelector('.subscribe-form-message');

if (!emailInput || !submitBtn) {
return;
}

var email = emailInput.value.trim();
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailPattern.test(email)) {
if (messageDiv) {
messageDiv.textContent = 'Please enter a valid email address.';
messageDiv.className = 'subscribe-form-message error';
messageDiv.style.display = 'block';
}
return;
}

// Hide any previous message and disable the button while submitting
if (messageDiv) {
messageDiv.style.display = 'none';
}
submitBtn.disabled = true;
var originalText = submitBtn.textContent;
submitBtn.textContent = 'Submitting...';

var formAction = form.getAttribute('action');
var formData = new FormData();
formData.append('email', email);

fetch(formAction, {
method: 'POST',
body: formData,
redirect: 'manual',
})
.then(function (response) {
// Customer.io returns 302 on success which becomes an opaque redirect
// with redirect:'manual'. Both 302 and opaque (type 0) indicate success.
if (response.ok || response.status === 302 || response.status === 0 || response.type === 'opaqueredirect') {
if (messageDiv) {
messageDiv.textContent = 'Thanks for subscribing! You will receive product updates at ' + email + '.';
messageDiv.className = 'subscribe-form-message success';
messageDiv.style.display = 'block';
}
emailInput.value = '';
} else {
throw new Error('Unexpected response: ' + response.status);
}
})
.catch(function () {
if (messageDiv) {
messageDiv.textContent = 'Something went wrong. Please try again.';
messageDiv.className = 'subscribe-form-message error';
messageDiv.style.display = 'block';
}
})
.finally(function () {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
}

function initializeAll() {
initializeHockeyStack();
initializeReo();
initializeHubSpot();
configurePostHog();
initializeSubscribeForm();
if (ENABLE_VOICE_WIDGET) {
injectVapiWidget();
}
}

// Fern uses client-side routing, so the form may appear after the initial page
// load. Re-attach the handler whenever the DOM changes on the whats-new page.
var subscribeFormObserver = new MutationObserver(function () {
if (window.location.pathname.indexOf('whats-new') !== -1) {
initializeSubscribeForm();
}
});

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeAll);
document.addEventListener('DOMContentLoaded', function () {
initializeAll();
subscribeFormObserver.observe(document.body, { childList: true, subtree: true });
});
} else {
initializeAll();
}
subscribeFormObserver.observe(document.body, { childList: true, subtree: true });
}
Loading
Loading