All checks were successful
Build and Push Docker Image / docker (push) Successful in 42s
## Stripe Payment Integration - Add Express.js backend server with Stripe Checkout Sessions - Create SQLite database for order tracking - Implement Stripe webhook handlers for payment events - Integrate with Wren Climate API for carbon offset fulfillment - Add CheckoutSuccess and CheckoutCancel pages - Create checkout API client for frontend - Update OffsetOrder component to redirect to Stripe Checkout - Add processing fee calculation (3% of base amount) - Implement order status tracking (pending → paid → fulfilled) Backend (server/): - Express server with CORS and middleware - SQLite database with Order schema - Stripe configuration and client - Order CRUD operations model - Checkout session creation endpoint - Webhook handler for payment confirmation - Wren API client for offset fulfillment Frontend: - CheckoutSuccess page with order details display - CheckoutCancel page with retry encouragement - Updated OffsetOrder to use Stripe checkout flow - Added checkout routes to App.tsx - TypeScript interfaces for checkout flow ## Visual & UX Enhancements - Add CertificationBadge component for project verification status - Create PortfolioDonutChart for visual portfolio allocation - Implement RadialProgress for percentage displays - Add reusable form components (FormInput, FormTextarea, FormSelect, FormFieldWrapper) - Refactor OffsetOrder with improved layout and animations - Add offset percentage slider with visual feedback - Enhance MobileOffsetOrder with better responsive design - Improve TripCalculator with cleaner UI structure - Update CurrencySelect with better styling - Add portfolio distribution visualization - Enhance project cards with hover effects and animations - Improve color palette and gradient usage throughout ## Configuration - Add VITE_API_BASE_URL environment variable - Create backend .env.example template - Update frontend .env.example with API URL - Add Stripe documentation references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
import React, { ReactNode } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { AlertCircle } from 'lucide-react';
|
|
|
|
interface FormFieldWrapperProps {
|
|
id: string;
|
|
label: string;
|
|
icon?: ReactNode;
|
|
error?: string;
|
|
isFilled: boolean;
|
|
isFocused: boolean;
|
|
disabled?: boolean;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function FormFieldWrapper({
|
|
id,
|
|
label,
|
|
icon,
|
|
error,
|
|
isFilled,
|
|
isFocused,
|
|
disabled,
|
|
children,
|
|
}: FormFieldWrapperProps) {
|
|
const isFloated = isFocused || isFilled;
|
|
const hasError = !!error;
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<div className="relative">
|
|
{/* Icon (if provided) */}
|
|
{icon && (
|
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-puffin-gray-label z-10">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
|
|
{/* Floating Label */}
|
|
<motion.label
|
|
htmlFor={id}
|
|
className={`
|
|
absolute left-3 pointer-events-none origin-left z-10
|
|
transition-colors duration-200
|
|
${icon ? 'pl-7' : ''}
|
|
${hasError ? 'text-puffin-error' : isFloated ? 'text-puffin-blue-focus' : 'text-puffin-gray-label'}
|
|
${disabled ? 'opacity-50' : ''}
|
|
`}
|
|
initial={false}
|
|
animate={{
|
|
y: isFloated ? -24 : 12,
|
|
scale: isFloated ? 0.85 : 1,
|
|
}}
|
|
transition={{
|
|
type: 'tween',
|
|
ease: 'easeOut',
|
|
duration: 0.2,
|
|
}}
|
|
>
|
|
{label}
|
|
</motion.label>
|
|
|
|
{/* Input Container with Border & Focus Effect */}
|
|
<div
|
|
className={`
|
|
relative rounded-lg transition-all duration-200
|
|
${hasError
|
|
? isFocused
|
|
? 'shadow-focus-red'
|
|
: ''
|
|
: isFocused
|
|
? 'shadow-focus-blue'
|
|
: ''
|
|
}
|
|
`}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
<AnimatePresence>
|
|
{hasError && (
|
|
<motion.div
|
|
id={`${id}-error`}
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="flex items-center gap-1.5 mt-1.5 text-sm text-puffin-error"
|
|
>
|
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
|
<span>{error}</span>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|