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>
148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Menu, X } from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import Image from 'next/image';
|
|
|
|
export function Header() {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [showHeader, setShowHeader] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let lastScroll = 0;
|
|
|
|
// Hide header on scroll down, show on scroll up
|
|
const handleScroll = () => {
|
|
const currentScrollY = window.scrollY;
|
|
|
|
// Always show header at the top of the page
|
|
if (currentScrollY < 10) {
|
|
setShowHeader(true);
|
|
} else if (currentScrollY > lastScroll) {
|
|
// Scrolling down
|
|
setShowHeader(false);
|
|
} else {
|
|
// Scrolling up
|
|
setShowHeader(true);
|
|
}
|
|
|
|
lastScroll = currentScrollY;
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []); // Empty dependency array - only set up once
|
|
|
|
const handleNavigate = (path: string) => {
|
|
setMobileMenuOpen(false);
|
|
router.push(path);
|
|
};
|
|
|
|
const navItems = [
|
|
{ path: '/calculator', label: 'Calculator' },
|
|
{ path: '/how-it-works', label: 'How it Works' },
|
|
{ path: '/about', label: 'About' },
|
|
{ path: '/contact', label: 'Contact' },
|
|
];
|
|
|
|
return (
|
|
<header
|
|
className="glass-nav shadow-luxury z-50 fixed top-0 left-0 right-0 transition-transform duration-300 ease-in-out"
|
|
style={{
|
|
transform: showHeader ? 'translate3d(0,0,0)' : 'translate3d(0,-100%,0)',
|
|
WebkitTransform: showHeader ? 'translate3d(0,0,0)' : 'translate3d(0,-100%,0)',
|
|
WebkitBackfaceVisibility: 'hidden',
|
|
backfaceVisibility: 'hidden'
|
|
}}
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 py-3 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between">
|
|
<motion.div
|
|
className="flex items-center space-x-3 cursor-pointer group"
|
|
onClick={() => handleNavigate('/')}
|
|
whileHover={{ scale: 1.02 }}
|
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
|
>
|
|
<motion.div
|
|
className="relative h-10 w-10"
|
|
initial={{ opacity: 0, rotate: -10 }}
|
|
animate={{ opacity: 1, rotate: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
>
|
|
<Image
|
|
src="/puffinOffset.webp"
|
|
alt="Puffin Offset Logo"
|
|
width={40}
|
|
height={40}
|
|
className="transition-transform duration-300 group-hover:scale-110"
|
|
/>
|
|
</motion.div>
|
|
<motion.h1
|
|
className="text-xl font-bold heading-luxury"
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.4 }}
|
|
>
|
|
Puffin Offset
|
|
</motion.h1>
|
|
</motion.div>
|
|
|
|
{/* Mobile menu button */}
|
|
<button
|
|
className="sm:hidden p-2 rounded-md text-gray-600 hover:text-gray-900"
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
aria-label="Toggle menu"
|
|
>
|
|
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
|
|
{/* Desktop navigation */}
|
|
<nav className="hidden sm:flex space-x-2">
|
|
{navItems.map((item, index) => (
|
|
<motion.button
|
|
key={item.path}
|
|
onClick={() => handleNavigate(item.path)}
|
|
className={`px-4 py-2 rounded-xl font-medium transition-all duration-300 ${
|
|
pathname === item.path
|
|
? 'bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg'
|
|
: 'text-slate-600 hover:text-slate-900 hover:bg-white/60'
|
|
}`}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.6 + index * 0.1 }}
|
|
>
|
|
{item.label}
|
|
</motion.button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Mobile navigation */}
|
|
{mobileMenuOpen && (
|
|
<nav className="sm:hidden mt-4 pb-2 space-y-2">
|
|
{navItems.map((item) => (
|
|
<button
|
|
key={item.path}
|
|
onClick={() => handleNavigate(item.path)}
|
|
className={`block w-full text-left px-4 py-2 rounded-lg ${
|
|
pathname === item.path
|
|
? 'bg-blue-50 text-blue-600 font-semibold'
|
|
: 'text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|