Some checks failed
Build and Push Docker Images / docker (push) Failing after 1m58s
This is a major migration from Vite to Next.js 16.0.1 for improved performance, better SEO, and modern React features. ## Next.js Migration Changes - Upgraded to Next.js 16.0.1 with Turbopack (from Vite 6) - Migrated from client-side routing to App Router architecture - Created app/ directory with Next.js page structure - Added server components and client components pattern - Configured standalone Docker builds for production ## Bug Fixes - React Hooks - Fixed infinite loop in Header.tsx scroll behavior (removed lastScrollY state dependency) - Fixed infinite loop in useCalculatorState.ts (wrapped saveState/clearState in useCallback) - Fixed infinite loop in OffsetOrder.tsx (removed savedState from useEffect dependencies) - Removed unused React imports from all client components ## Environment Variable Migration - Migrated all VITE_ variables to NEXT_PUBLIC_ prefix - Updated src/utils/config.ts to use direct static references (required for Next.js) - Updated src/api/checkoutClient.ts, emailClient.ts, aisClient.ts for Next.js env vars - Updated src/vite-env.d.ts types for Next.js environment - Maintained backward compatibility with Docker window.env ## Layout & UX Improvements - Fixed footer to always stay at bottom of viewport using flexbox - Updated app/layout.tsx with flex-1 main content area - Preserved glass morphism effects and luxury styling ## TypeScript & Build - Fixed TypeScript strict mode compilation errors - Removed unused imports and variables - Fixed Axios interceptor types in project/src/api/wrenClient.ts - Production build verified and passing ## Testing & Verification - Tested calculator end-to-end in Playwright - Verified Wren API integration working (11 portfolios fetched) - Confirmed calculation: 5000L → 13.47 tons CO₂ → $3,206 total - All navigation routes working correctly - Footer positioning verified across all pages ## Files Added - app/ directory with Next.js routes - components/ directory with client components - next.config.mjs, next-env.d.ts - ENV_MIGRATION.md, NEXTJS_MIGRATION_COMPLETE.md documentation ## Files Modified - Docker configuration for Next.js standalone builds - package.json dependencies (Next.js, React 19) - ts config.json for Next.js - All API clients for new env var pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { Mail, Phone, Loader2 } from 'lucide-react';
|
|
import { validateEmail, sendContactFormEmail } from '../src/utils/email';
|
|
import { analytics } from '../src/utils/analytics';
|
|
|
|
export function ContactClient() {
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
company: '',
|
|
message: ''
|
|
});
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const [sending, setSending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSending(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Validate email
|
|
if (!validateEmail(formData.email)) {
|
|
throw new Error('Please enter a valid email address');
|
|
}
|
|
|
|
// Send via SMTP backend
|
|
await sendContactFormEmail(formData, 'contact');
|
|
|
|
setSubmitted(true);
|
|
analytics.event('contact', 'form_submitted');
|
|
|
|
// Reset form after delay
|
|
setTimeout(() => {
|
|
setFormData({
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
company: '',
|
|
message: ''
|
|
});
|
|
setSubmitted(false);
|
|
}, 3000);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to send message. Please try again.');
|
|
analytics.error(err as Error, 'Contact form submission failed');
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="text-center mb-12">
|
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">Contact Us</h1>
|
|
<p className="text-xl text-gray-600">
|
|
Ready to start your sustainability journey? Get in touch with our team today.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl shadow-lg p-8 sm:p-12">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 sm:gap-12">
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">Get in Touch</h2>
|
|
<p className="text-gray-600 mb-8 text-justify">
|
|
Have questions about our carbon offsetting solutions? Our team is here to help you make a difference in maritime sustainability.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div className="flex items-center space-x-4">
|
|
<Mail className="text-blue-600" size={24} />
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">Email Us</h3>
|
|
<a
|
|
href="mailto:info@puffinoffset.com"
|
|
className="text-blue-600 hover:text-blue-700"
|
|
>
|
|
info@puffinoffset.com
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<Phone className="text-blue-600" size={24} />
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">Call Us</h3>
|
|
<a
|
|
href="tel:+33671187253"
|
|
className="text-blue-600 hover:text-blue-700"
|
|
>
|
|
+33 6 71 18 72 53
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{submitted && (
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
<p className="text-green-700">
|
|
Thank you for your message. Your email client will open shortly.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
<p className="text-red-700">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Email *
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Phone
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
id="phone"
|
|
name="phone"
|
|
value={formData.phone}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder="+1 234 567 8900"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Company
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="company"
|
|
name="company"
|
|
value={formData.company}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Message *
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
name="message"
|
|
value={formData.message}
|
|
onChange={handleChange}
|
|
rows={4}
|
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
></textarea>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={sending}
|
|
className={`w-full flex items-center justify-center bg-blue-600 text-white py-3 rounded-lg transition-colors ${
|
|
sending ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-700'
|
|
}`}
|
|
>
|
|
{sending ? (
|
|
<>
|
|
<Loader2 className="animate-spin mr-2" size={20} />
|
|
Preparing Email...
|
|
</>
|
|
) : (
|
|
'Send Message'
|
|
)}
|
|
</button>
|
|
<p className="text-sm text-gray-500 text-center">
|
|
* Required fields
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|