puffin-app/components/Header.tsx
Matt 82f72941ca
Some checks failed
Build and Push Docker Images / docker (push) Failing after 1m58s
Migrate from Vite to Next.js 16 with Turbopack
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>
2025-10-31 22:23:45 +01:00

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>
);
}