nicholai-work-2026/src/pages/contact.astro
Nicholai 0cc1612fbb Add open-source VFX pipeline guide and enhance site structure
- Introduced a comprehensive guide on building a production-ready VFX pipeline using open-source tools, detailing cost breakdowns and workflows.
- Added new sections for experience, featured projects, skills, and contact information, enhancing the overall site structure and user navigation.
- Updated components to utilize dynamic content from new markdown files, improving maintainability and scalability of the site.
- Enhanced the contact page with structured data for better user interaction and accessibility.
2025-12-08 01:29:03 -07:00

711 lines
31 KiB
Plaintext

---
import BaseLayout from '../layouts/BaseLayout.astro';
import { SITE_TITLE } from '../consts';
import { getEntry } from 'astro:content';
const pageTitle = `Contact | ${SITE_TITLE}`;
// Fetch contact page content
const contactEntry = await getEntry('pages', 'contact');
const contactContent = contactEntry.data;
---
<BaseLayout title={pageTitle} description="Get in touch for collaboration or inquiries." usePadding={false}>
<!-- Background Grid (Optional, low opacity) -->
<div class="fixed inset-0 z-0 pointer-events-none">
<div class="w-full h-full grid grid-cols-12 gap-4 opacity-[0.03]">
{Array.from({ length: 12 }).map((_) => (
<div class="h-full border-r border-white"></div>
))}
</div>
</div>
<section class="relative z-10 min-h-screen flex flex-col pt-32 lg:pt-48 pb-20 px-6 lg:px-12">
<!-- Page Header -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 mb-20 lg:mb-32 border-b border-white/10 pb-12">
<div class="lg:col-span-8">
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold uppercase tracking-tighter leading-[0.85] text-white mb-8">
<span class="block animate-on-scroll slide-up">{contactContent.pageTitleLine1}</span>
<span class="block text-brand-accent animate-on-scroll slide-up stagger-1">{contactContent.pageTitleLine2}</span>
</h1>
</div>
<div class="lg:col-span-4 flex flex-col justify-end">
<p class="font-mono text-sm text-slate-400 leading-relaxed mb-8 border-l border-brand-accent pl-6 animate-on-scroll fade-in stagger-2">
{contactContent.availabilityText}
</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-24 flex-grow">
<!-- Left Column: Contact Form -->
<div class="lg:col-span-7 animate-on-scroll slide-up stagger-3">
<div class="mb-8 flex items-center gap-3">
<span class="w-2 h-2 bg-brand-accent rounded-full animate-pulse"></span>
<span class="font-mono text-xs text-brand-accent uppercase tracking-widest">{contactContent.formLabels?.transmissionUplink}</span>
</div>
<form id="contact-form" class="space-y-12">
<div class="group relative">
<input
type="text"
id="name"
name="name"
class="block w-full bg-transparent border-b border-white/20 py-4 text-xl text-white focus:outline-none focus:border-brand-accent transition-colors duration-300 placeholder-transparent peer"
placeholder="Name"
required
/>
<label for="name" class="absolute left-0 top-4 text-slate-500 text-sm font-mono uppercase tracking-widest transition-all duration-300 peer-focus:-top-6 peer-focus:text-xs peer-focus:text-brand-accent peer-valid:-top-6 peer-valid:text-xs peer-valid:text-slate-400 pointer-events-none">
{contactContent.formLabels?.name}
</label>
</div>
<div class="group relative">
<input
type="email"
id="email"
name="email"
class="block w-full bg-transparent border-b border-white/20 py-4 text-xl text-white focus:outline-none focus:border-brand-accent transition-colors duration-300 placeholder-transparent peer"
placeholder="Email"
required
/>
<label for="email" class="absolute left-0 top-4 text-slate-500 text-sm font-mono uppercase tracking-widest transition-all duration-300 peer-focus:-top-6 peer-focus:text-xs peer-focus:text-brand-accent peer-valid:-top-6 peer-valid:text-xs peer-valid:text-slate-400 pointer-events-none">
{contactContent.formLabels?.email}
</label>
</div>
<!-- Custom Dropdown -->
<div class="group relative" id="custom-select">
<input type="hidden" name="subject" id="subject-input" required>
<button type="button" id="select-trigger" class="block w-full text-left bg-transparent border-b border-white/20 py-4 text-xl text-white focus:outline-none focus:border-brand-accent transition-colors duration-300 flex justify-between items-center group-hover:border-white/40">
<span id="select-value" class="text-transparent">Select</span> <!-- Hidden placeholder text to keep height -->
<div class="text-brand-accent transform transition-transform duration-300" id="select-arrow">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</div>
</button>
<label id="select-label" class="absolute left-0 top-4 text-slate-500 text-sm font-mono uppercase tracking-widest transition-all duration-300 pointer-events-none">
{contactContent.formLabels?.subject}
</label>
<!-- Dropdown Menu -->
<div id="select-options" class="absolute left-0 top-full w-full bg-brand-dark border border-white/20 shadow-2xl z-50 hidden opacity-0 transform translate-y-2 transition-all duration-200 origin-top mt-2">
<div class="p-1">
{contactContent.subjectOptions?.map((option) => (
<div class="option px-5 py-4 hover:bg-white/5 cursor-pointer text-white text-lg font-light transition-colors flex items-center gap-3 group/option" data-value={option.value}>
<span class="w-1.5 h-1.5 rounded-full bg-brand-accent opacity-0 group-hover/option:opacity-100 transition-opacity"></span>
{option.label}
</div>
))}
</div>
</div>
</div>
<div class="group relative">
<textarea
id="message"
name="message"
rows="4"
class="block w-full bg-transparent border-b border-white/20 py-4 text-xl text-white focus:outline-none focus:border-brand-accent transition-colors duration-300 placeholder-transparent peer resize-none"
placeholder="Message"
required
></textarea>
<label for="message" class="absolute left-0 top-4 text-slate-500 text-sm font-mono uppercase tracking-widest transition-all duration-300 peer-focus:-top-6 peer-focus:text-xs peer-focus:text-brand-accent peer-valid:-top-6 peer-valid:text-xs peer-valid:text-slate-400 pointer-events-none">
{contactContent.formLabels?.message}
</label>
</div>
<div class="pt-8">
<button type="submit" id="submit-btn" class="group relative inline-flex items-center justify-center gap-4 px-8 py-4 bg-transparent border border-white/20 hover:border-brand-accent hover:bg-brand-accent/5 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed">
<span id="submit-text" data-default-text={contactContent.formLabels?.submit} class="font-mono text-xs font-bold uppercase tracking-widest text-white group-hover:text-brand-accent transition-colors">{contactContent.formLabels?.submit}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-slate-500 group-hover:text-brand-accent group-hover:translate-x-1 transition-all"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</form>
</div>
<!-- Right Column: System Status / Info -->
<div class="lg:col-span-5 space-y-16 animate-on-scroll slide-left stagger-4">
<!-- Data Block 1 -->
<div class="relative pl-6 border-l border-white/10">
<h3 class="font-mono text-xs text-slate-500 uppercase tracking-widest mb-4">Direct Link</h3>
<a href={`mailto:${contactContent.email}`} class="text-2xl md:text-3xl font-bold text-white hover:text-brand-accent transition-colors break-all">
{contactContent.email}
</a>
</div>
<!-- Data Block 2 -->
<div class="relative pl-6 border-l border-white/10">
<h3 class="font-mono text-xs text-slate-500 uppercase tracking-widest mb-4">Coordinates</h3>
<p class="text-xl text-white font-light">
{contactContent.location}<br>
<span class="text-slate-500 text-base">{contactContent.locationCountry}</span>
</p>
<div class="mt-4 font-mono text-xs text-brand-accent">
{contactContent.coordinates}
</div>
</div>
<!-- Data Block 3 -->
<div class="relative pl-6 border-l border-white/10">
<h3 class="font-mono text-xs text-slate-500 uppercase tracking-widest mb-4">Social Feed</h3>
<ul class="space-y-4">
{contactContent.socialLinks?.map((link) => (
<li>
<a href={link.url} class="flex items-center gap-4 group">
<span class="text-slate-400 group-hover:text-white transition-colors text-lg">{link.name}</span>
<svg class="w-4 h-4 text-slate-600 group-hover:text-brand-accent transition-colors transform group-hover:translate-x-1 group-hover:-translate-y-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>
</a>
</li>
))}
</ul>
</div>
</div>
</div>
</section>
<!-- Toast Notification Container -->
<div id="toast-container" class="fixed top-6 right-6 z-50 pointer-events-none"></div>
<!-- Full-Screen Modal for Loading and Response -->
<div id="transmission-modal" class="fixed inset-0 z-[100] flex items-center justify-center bg-brand-dark/95 backdrop-blur-xl opacity-0 pointer-events-none transition-opacity duration-500">
<!-- Loading State -->
<div id="loading-state" class="text-center">
<!-- Animated Transmission Graphic -->
<div class="relative w-32 h-32 mx-auto mb-12">
<!-- Outer rotating ring -->
<div class="absolute inset-0 border-2 border-brand-accent/20 rounded-full animate-spin-slow"></div>
<!-- Middle pulsing ring -->
<div class="absolute inset-4 border-2 border-brand-accent/40 rounded-full animate-pulse"></div>
<!-- Inner dot -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-4 h-4 bg-brand-accent rounded-full animate-ping"></div>
<div class="w-4 h-4 bg-brand-accent rounded-full absolute"></div>
</div>
<!-- Signal waves -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-full h-0.5 bg-gradient-to-r from-transparent via-brand-accent to-transparent animate-pulse"></div>
</div>
<div class="absolute inset-0 flex items-center justify-center rotate-90">
<div class="w-full h-0.5 bg-gradient-to-r from-transparent via-brand-accent to-transparent animate-pulse delay-150"></div>
</div>
</div>
<!-- Loading Text -->
<div class="space-y-4">
<h2 class="text-3xl md:text-5xl font-bold text-white uppercase tracking-tight">
<span id="loading-text">Transmitting</span>
</h2>
<div class="flex items-center justify-center gap-2">
<div class="w-2 h-2 bg-brand-accent rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-brand-accent rounded-full animate-bounce delay-100"></div>
<div class="w-2 h-2 bg-brand-accent rounded-full animate-bounce delay-200"></div>
</div>
<p class="font-mono text-sm text-slate-400 uppercase tracking-widest">Signal Processing</p>
</div>
</div>
<!-- Response State (hidden initially) -->
<div id="response-state" class="hidden w-full h-full absolute inset-0 z-10 flex flex-col items-center justify-center p-6 opacity-0 transition-all duration-700">
<!-- Close button -->
<button id="close-modal" class="absolute top-8 right-8 z-50 p-3 border border-white/20 hover:border-brand-accent hover:bg-brand-accent/10 transition-all duration-300 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-white group-hover:text-brand-accent transition-colors">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<!-- Content Container -->
<div class="w-full max-w-5xl mx-auto flex flex-col items-center relative">
<!-- Header - More subtle now -->
<div class="text-center mb-12 animate-scale-in">
<div class="flex items-center justify-center gap-3 mb-4">
<span class="w-2 h-2 bg-brand-accent rounded-full animate-pulse"></span>
<p class="font-mono text-sm text-brand-accent uppercase tracking-widest">Transmission Received</p>
<span class="w-2 h-2 bg-brand-accent rounded-full animate-pulse"></span>
</div>
</div>
<!-- Response content - The Focal Point -->
<div class="w-full relative">
<!-- Decorative corner markers -->
<div class="absolute -top-4 -left-4 w-8 h-8 border-t-2 border-l-2 border-brand-accent opacity-50"></div>
<div class="absolute -top-4 -right-4 w-8 h-8 border-t-2 border-r-2 border-brand-accent opacity-50"></div>
<div class="absolute -bottom-4 -left-4 w-8 h-8 border-b-2 border-l-2 border-brand-accent opacity-50"></div>
<div class="absolute -bottom-4 -right-4 w-8 h-8 border-b-2 border-r-2 border-brand-accent opacity-50"></div>
<!-- Content -->
<div id="response-content" class="prose-response max-h-[70vh] overflow-y-auto custom-scrollbar px-4 md:px-8 py-4"></div>
</div>
</div>
</div>
</div>
</BaseLayout>
<style>
/* Custom autofill styles to match dark theme */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus {
-webkit-text-fill-color: white;
-webkit-box-shadow: 0 0 0px 1000px #0B0D11 inset;
transition: background-color 5000s ease-in-out 0s;
}
/* Label active state */
.label-active {
top: -1.5rem !important;
font-size: 0.75rem !important;
color: #94A3B8 !important;
}
/* Dropdown open state */
.dropdown-open #select-arrow {
transform: rotate(180deg);
color: white;
}
/* Custom Animations */
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes scale-in {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.animate-spin-slow {
animation: spin-slow 3s linear infinite;
}
.animate-scale-in {
animation: scale-in 0.6s ease-out forwards;
}
.delay-100 {
animation-delay: 100ms;
}
.delay-150 {
animation-delay: 150ms;
}
.delay-200 {
animation-delay: 200ms;
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 255, 255, 0.3);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0, 255, 255, 0.5);
}
/* Response Content Prose Styles - Enhanced Readability */
.prose-response {
color: white;
text-align: left;
}
.prose-response h1,
.prose-response h2,
.prose-response h3 {
color: white;
margin-top: 1.5em;
margin-bottom: 0.75em;
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
text-transform: uppercase;
}
.prose-response h1 {
font-size: 3.5rem;
background: linear-gradient(to right, #fff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5em;
}
.prose-response h2 {
font-size: 2.5rem;
}
.prose-response h3 {
font-size: 1.75rem;
color: #ff4d00;
}
.prose-response p {
margin-bottom: 1.5em;
line-height: 1.8;
color: rgba(255, 255, 255, 0.9);
font-size: 1.5rem; /* Increased size significantly */
font-weight: 300;
max-width: 65ch;
margin-right: auto;
}
.prose-response strong {
color: #ff4d00;
font-weight: 600;
}
.prose-response em {
font-style: italic;
color: #94a3b8;
}
/* Blockquote for signature or special text */
.prose-response blockquote {
border-left: none; /* Removed standard border */
margin: 3em 0 1em;
padding: 0;
color: #ff4d00;
font-family: 'Courier New', monospace;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.2em;
display: inline-block;
border-top: 1px solid rgba(255, 77, 0, 0.3);
padding-top: 2em;
}
.prose-response a {
color: #ff4d00;
text-decoration: underline;
text-underline-offset: 4px;
transition: all 0.3s;
}
.prose-response a:hover {
color: white;
text-decoration-thickness: 2px;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.prose-response h1 {
font-size: 2.5rem;
}
.prose-response p {
font-size: 1.125rem;
}
}
</style>
<script>
import { marked } from 'marked';
// ===== Custom Dropdown Logic =====
const selectContainer = document.getElementById('custom-select');
const selectTrigger = document.getElementById('select-trigger');
const selectOptions = document.getElementById('select-options');
const selectValue = document.getElementById('select-value');
const hiddenInput = document.getElementById('subject-input') as HTMLInputElement;
const selectLabel = document.getElementById('select-label');
const options = document.querySelectorAll('.option');
const arrow = document.getElementById('select-arrow');
if (selectTrigger && selectOptions && selectValue && hiddenInput && selectLabel) {
// Toggle Dropdown
selectTrigger.addEventListener('click', () => {
const isOpen = !selectOptions.classList.contains('hidden');
if (isOpen) {
closeDropdown();
} else {
openDropdown();
}
});
// Option Selection
options.forEach(option => {
option.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const value = target.dataset.value || '';
const text = target.innerText;
// Update UI
selectValue.textContent = text;
selectValue.classList.remove('text-transparent');
selectValue.classList.add('text-white');
// Update Data
hiddenInput.value = value;
// Update Label Style
selectLabel.classList.add('label-active');
selectLabel.classList.add('text-brand-accent'); // Highlight when selected
closeDropdown();
});
});
// Close clicking outside
document.addEventListener('click', (e) => {
if (selectContainer && !selectContainer.contains(e.target as Node)) {
closeDropdown();
}
});
function openDropdown() {
selectOptions?.classList.remove('hidden');
// Small delay for opacity transition
requestAnimationFrame(() => {
selectOptions?.classList.remove('opacity-0', 'translate-y-2');
});
selectContainer?.classList.add('dropdown-open');
}
function closeDropdown() {
selectOptions?.classList.add('opacity-0', 'translate-y-2');
setTimeout(() => {
selectOptions?.classList.add('hidden');
}, 200);
selectContainer?.classList.remove('dropdown-open');
}
}
// ===== Toast Notification System =====
function showToast(message: string, type: 'success' | 'error' = 'error') {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `pointer-events-auto mb-4 p-4 border backdrop-blur-sm transform transition-all duration-300 translate-x-full opacity-0 ${
type === 'success'
? 'bg-green-500/10 border-green-500/30 text-green-400'
: 'bg-red-500/10 border-red-500/30 text-red-400'
}`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="flex items-start gap-3 max-w-sm">
<div class="flex-shrink-0">
${type === 'success'
? '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
}
</div>
<p class="text-sm font-mono">${message}</p>
</div>
`;
container.appendChild(toast);
// Trigger animation
requestAnimationFrame(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
});
// Auto-dismiss after 6 seconds
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, 6000);
}
// ===== Modal Control Functions =====
const transmissionModal = document.getElementById('transmission-modal') as HTMLDivElement;
const loadingState = document.getElementById('loading-state') as HTMLDivElement;
const responseState = document.getElementById('response-state') as HTMLDivElement;
const closeModalBtn = document.getElementById('close-modal') as HTMLButtonElement;
function openModal() {
transmissionModal.classList.remove('pointer-events-none', 'opacity-0');
transmissionModal.classList.add('pointer-events-auto', 'opacity-100');
document.body.style.overflow = 'hidden';
}
function closeModal() {
transmissionModal.classList.add('pointer-events-none', 'opacity-0');
transmissionModal.classList.remove('pointer-events-auto', 'opacity-100');
document.body.style.overflow = '';
// Reset states after animation
setTimeout(() => {
loadingState.classList.remove('hidden');
responseState.classList.add('hidden');
responseState.classList.remove('opacity-100', 'scale-100');
responseState.classList.add('opacity-0', 'scale-95');
}, 500);
}
function showResponse() {
loadingState.classList.add('hidden');
responseState.classList.remove('hidden');
// Trigger animation after a brief delay
setTimeout(() => {
responseState.classList.remove('opacity-0', 'scale-95');
responseState.classList.add('opacity-100', 'scale-100');
}, 100);
}
// Modal close handler
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeModal);
}
// Close on click outside
if (transmissionModal) {
transmissionModal.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
// Only close if response state is active and visible
// We check if the click target is the container itself (the background)
// response-state covers the whole screen when active
if (!responseState.classList.contains('hidden') &&
(target === responseState || target === transmissionModal)) {
closeModal();
}
});
}
// ===== Form Submission Handler =====
const contactForm = document.getElementById('contact-form') as HTMLFormElement;
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement;
const submitText = document.getElementById('submit-text') as HTMLSpanElement;
const responseContent = document.getElementById('response-content') as HTMLDivElement;
if (contactForm && submitBtn && submitText && responseContent && transmissionModal) {
contactForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Get form data
const formData = new FormData(contactForm);
const payload = {
name: formData.get('name') as string,
email: formData.get('email') as string,
subject: formData.get('subject') as string,
message: formData.get('message') as string,
timestamp: new Date().toISOString(),
source: 'portfolio-website'
};
// Open modal with loading state
openModal();
// Disable submit button
submitBtn.disabled = true;
submitText.textContent = 'Transmitting...';
try {
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
// Make the request
const response = await fetch(import.meta.env.PUBLIC_N8N_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: controller.signal
});
clearTimeout(timeoutId);
// Check for non-2xx response
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
// Parse response
const data = await response.json();
// Check if n8n returned an error
if (data.success === false) {
throw new Error(data.error || 'An error occurred processing your message');
}
// Success path - render markdown response
if (data.success && data.format === 'mdx' && data.message) {
try {
const htmlContent = await marked.parse(data.message);
responseContent.innerHTML = htmlContent;
// Show response state with animation
showResponse();
// Reset button state
submitText.textContent = submitText.getAttribute('data-default-text') || 'Transmit Message';
submitBtn.disabled = false;
} catch (markdownError) {
console.error('Markdown parsing error:', markdownError);
throw new Error('Failed to render response');
}
} else {
throw new Error('Invalid response format from server');
}
} catch (error) {
console.error('Form submission error:', error);
// Close modal
closeModal();
// Determine error message
let errorMessage = 'We couldn\'t reach the messaging system. Please try again or email me directly at nicholai@nicholai.work';
if (error instanceof Error) {
if (error.name === 'AbortError') {
errorMessage = 'Request timed out. Please try again or email me directly at nicholai@nicholai.work';
} else if (error.message && !error.message.includes('Server returned')) {
errorMessage = error.message;
}
}
// Show error toast
showToast(errorMessage, 'error');
// Update button to failure state
submitText.textContent = 'Transmission Failed';
const defaultText = submitText.getAttribute('data-default-text') || 'Transmit Message';
setTimeout(() => {
submitText.textContent = defaultText;
submitBtn.disabled = false;
}, 2000);
// Keep form data intact (don't reset)
}
});
}
</script>