1070 lines
45 KiB
TypeScript
1070 lines
45 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { Camera, ArrowRight, Edit3, Upload, Type, Calendar, Palette, Edit, Stamp } from 'lucide-react';
|
|
import { Button } from './ui/button';
|
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
import { motion, useMotionValue, useSpring, useTransform, useInView } from 'motion/react';
|
|
import { HandwrittenText, useHandwrittenText } from './HandwrittenText';
|
|
import front from '../assets/front.jpg'
|
|
|
|
interface EditableCardProps {
|
|
isEditing: boolean;
|
|
onEdit: () => void;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
editIcon?: React.ReactNode;
|
|
}
|
|
|
|
export function CustomPostcards() {
|
|
const [editingCard, setEditingCard] = useState<string | null>(null);
|
|
const [isFlipped, setIsFlipped] = useState(false);
|
|
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
|
|
const postcardRef = useRef<HTMLDivElement>(null);
|
|
const sectionRef = useRef<HTMLDivElement>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// 3D tilt effect using mouse position
|
|
const mouseX = useMotionValue(0);
|
|
const mouseY = useMotionValue(0);
|
|
|
|
// Spring animations for smooth mouse following
|
|
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [5, -5]), {
|
|
stiffness: 100,
|
|
damping: 15
|
|
});
|
|
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-5, 5]), {
|
|
stiffness: 100,
|
|
damping: 15
|
|
});
|
|
|
|
// Detect when section is in view to trigger handwriting
|
|
const isInView = useInView(sectionRef, {
|
|
once: true,
|
|
margin: "-100px",
|
|
amount: 0.3
|
|
});
|
|
|
|
// Handwritten text control
|
|
const handwrittenControl = useHandwrittenText(false);
|
|
|
|
// Handle mouse movement for 3D effect (disabled when flipped)
|
|
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!postcardRef.current || isFlipped) return;
|
|
|
|
const rect = postcardRef.current.getBoundingClientRect();
|
|
const centerX = rect.left + rect.width / 2;
|
|
const centerY = rect.top + rect.height / 2;
|
|
|
|
const x = (event.clientX - centerX) / (rect.width / 2);
|
|
const y = (event.clientY - centerY) / (rect.height / 2);
|
|
|
|
mouseX.set(x);
|
|
mouseY.set(y);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
if (isFlipped) return;
|
|
mouseX.set(0);
|
|
mouseY.set(0);
|
|
};
|
|
|
|
const [postcardData, setPostcardData] = useState({
|
|
photo: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop&crop=center",
|
|
message: "Greetings from paradise!\\nThe beaches here are absolutely\\nbreathtaking. Wish you were\\nhere to enjoy this amazing\\nsunset with me.",
|
|
date: "July 2024",
|
|
addressLabel: "POSTCARD"
|
|
});
|
|
|
|
const handleCreatePostcard = () => {
|
|
console.log('Navigate to postcard creation page...');
|
|
};
|
|
|
|
// Handle image upload
|
|
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setUploadedImage(reader.result as string);
|
|
// Automatically flip to show the uploaded image
|
|
setIsFlipped(true);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
// Trigger file input
|
|
const triggerFileInput = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
// Start handwriting animation when section comes into view
|
|
useEffect(() => {
|
|
if (isInView && !editingCard) {
|
|
// Delay the start of handwriting to let the postcard animation settle
|
|
const timer = setTimeout(() => {
|
|
handwrittenControl.start();
|
|
}, 1200);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [isInView, editingCard, handwrittenControl]);
|
|
|
|
// Reset handwriting when editing
|
|
useEffect(() => {
|
|
if (editingCard === 'message') {
|
|
handwrittenControl.reset();
|
|
}
|
|
}, [editingCard, handwrittenControl]);
|
|
|
|
// Reset 3D tilt when card is flipped
|
|
useEffect(() => {
|
|
if (isFlipped) {
|
|
mouseX.set(0);
|
|
mouseY.set(0);
|
|
}
|
|
}, [isFlipped, mouseX, mouseY]);
|
|
|
|
const EditableCard = ({ isEditing, onEdit, children, className = "", style = {}, editIcon }: EditableCardProps) => {
|
|
return (
|
|
<motion.div
|
|
className={`relative group cursor-pointer ${className}`}
|
|
style={style}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEdit();
|
|
}}
|
|
whileHover={{
|
|
scale: 1.02,
|
|
transition: { duration: 0.2 }
|
|
}}
|
|
whileTap={{ scale: 0.98 }}
|
|
animate={isEditing ? {
|
|
boxShadow: "0 0 0 2px rgba(249, 95, 98, 0.5), 0 8px 16px rgba(249, 95, 98, 0.3)"
|
|
} : {}}
|
|
>
|
|
{children}
|
|
|
|
{/* Animated Edit overlay */}
|
|
<motion.div
|
|
className="absolute inset-0 bg-black rounded-md flex items-center justify-center"
|
|
initial={{ opacity: 0, backgroundColor: "rgba(0, 0, 0, 0)" }}
|
|
animate={isEditing ? {
|
|
opacity: 1,
|
|
backgroundColor: "rgba(0, 0, 0, 0.2)"
|
|
} : {
|
|
opacity: 0,
|
|
backgroundColor: "rgba(0, 0, 0, 0)"
|
|
}}
|
|
whileHover={!isEditing ? {
|
|
opacity: 1,
|
|
backgroundColor: "rgba(0, 0, 0, 0.1)"
|
|
} : {}}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<motion.div
|
|
className="bg-white bg-opacity-90 px-2 py-1 rounded-md shadow-lg border border-gray-200 flex items-center gap-1"
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
whileHover={{ scale: 1.05 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<motion.div
|
|
animate={isEditing ? { rotate: [0, 10, -10, 0] } : {}}
|
|
transition={{ duration: 2, repeat: isEditing ? Infinity : 0 }}
|
|
>
|
|
{editIcon}
|
|
</motion.div>
|
|
<motion.span
|
|
className="text-xs text-gray-700 font-medium"
|
|
animate={isEditing ? {
|
|
color: ["#374151", "#F95F62", "#374151"]
|
|
} : {}}
|
|
transition={{ duration: 1, repeat: isEditing ? Infinity : 0 }}
|
|
>
|
|
{isEditing ? 'Editing...' : 'Edit'}
|
|
</motion.span>
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
// Ultra-realistic vintage postcard with responsive scaling and animations
|
|
// const PostcardFrame = () => {
|
|
// return (
|
|
// <motion.div
|
|
// className="relative shadow-2xl transform rotate-[0.5deg] w-full h-full"
|
|
// initial={{ opacity: 0, scale: 0.9, rotate: 0 }}
|
|
// animate={{ opacity: 1, scale: 1, rotate: 0.5 }}
|
|
// transition={{ duration: 0.8, ease: "easeOut" }}
|
|
// style={{
|
|
// background: `
|
|
// radial-gradient(circle at 20% 80%, rgba(210, 180, 140, 0.15) 0%, transparent 50%),
|
|
// radial-gradient(circle at 80% 20%, rgba(160, 130, 100, 0.1) 0%, transparent 50%),
|
|
// radial-gradient(circle at 40% 40%, rgba(190, 160, 120, 0.08) 0%, transparent 50%),
|
|
// linear-gradient(135deg, #f8f4e6 0%, #f0e7d3 25%, #ede2d0 50%, #e8dcc8 75%, #e3d5c2 100%)
|
|
// `,
|
|
// borderRadius: '10px',
|
|
// boxShadow: `
|
|
// inset 0px 1px 30px rgba(139, 115, 85, 0.2),
|
|
// inset 0px -1px 20px rgba(160, 130, 100, 0.15),
|
|
// 0px 12px 40px rgba(0, 0, 0, 0.15),
|
|
// 0px 4px 12px rgba(0, 0, 0, 0.1)
|
|
// `,
|
|
// border: '1px solid rgba(139, 115, 85, 0.2)'
|
|
// }}
|
|
// >
|
|
// {/* Realistic paper texture with variations */}
|
|
// <div
|
|
// className="absolute inset-0 opacity-40 rounded-[10px] pointer-events-none"
|
|
// style={{
|
|
// backgroundImage: `
|
|
// url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23d4af9a' fill-opacity='0.08'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"),
|
|
// radial-gradient(circle at 25% 75%, rgba(180, 150, 120, 0.1) 0%, transparent 40%),
|
|
// radial-gradient(circle at 75% 25%, rgba(160, 130, 100, 0.08) 0%, transparent 35%)
|
|
// `
|
|
// }}
|
|
// />
|
|
|
|
// {/* Age spots and stains */}
|
|
// <div className="absolute inset-0 rounded-[10px] pointer-events-none opacity-30">
|
|
// <div
|
|
// className="absolute w-8 h-6 rounded-full"
|
|
// style={{
|
|
// background: 'radial-gradient(ellipse, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
|
|
// top: '15%',
|
|
// right: '20%',
|
|
// transform: 'rotate(-15deg)'
|
|
// }}
|
|
// />
|
|
// <div
|
|
// className="absolute w-6 h-8 rounded-full"
|
|
// style={{
|
|
// background: 'radial-gradient(ellipse, rgba(140, 110, 70, 0.12) 0%, transparent 60%)',
|
|
// bottom: '25%',
|
|
// left: '15%',
|
|
// transform: 'rotate(25deg)'
|
|
// }}
|
|
// />
|
|
// <div
|
|
// className="absolute w-4 h-4 rounded-full"
|
|
// style={{
|
|
// background: 'radial-gradient(circle, rgba(180, 140, 100, 0.2) 0%, transparent 50%)',
|
|
// top: '60%',
|
|
// right: '10%'
|
|
// }}
|
|
// />
|
|
// </div>
|
|
|
|
// {/* Corner wear and creases */}
|
|
// <div
|
|
// className="absolute top-0 right-0 w-12 h-12 opacity-25"
|
|
// style={{
|
|
// background: 'radial-gradient(circle at top right, rgba(139, 115, 85, 0.3) 20%, rgba(160, 130, 100, 0.2) 40%, transparent 70%)',
|
|
// borderTopRightRadius: '10px'
|
|
// }}
|
|
// />
|
|
// <div
|
|
// className="absolute bottom-0 left-0 w-16 h-16 opacity-20"
|
|
// style={{
|
|
// background: 'radial-gradient(circle at bottom left, rgba(120, 95, 70, 0.25) 25%, rgba(140, 115, 85, 0.15) 50%, transparent 75%)',
|
|
// borderBottomLeftRadius: '10px'
|
|
// }}
|
|
// />
|
|
|
|
// {/* Subtle crease lines */}
|
|
// <div
|
|
// className="absolute w-full h-px bg-gradient-to-r from-transparent via-amber-800/10 to-transparent opacity-40"
|
|
// style={{
|
|
// top: '35%',
|
|
// transform: 'rotate(-0.5deg)'
|
|
// }}
|
|
// />
|
|
|
|
// {/* Animated Vintage Vector Logo - Top Right - Mobile Optimized */}
|
|
// <motion.div
|
|
// className="absolute opacity-60 pointer-events-none"
|
|
// style={{
|
|
// top: '4.3%',
|
|
// right: '3.5%',
|
|
// width: '7.5%',
|
|
// height: '11.5%',
|
|
// filter: 'sepia(30%) saturate(120%) hue-rotate(15deg) brightness(85%) contrast(110%)'
|
|
// }}
|
|
// animate={{
|
|
// rotate: [5, 7, 3, 5],
|
|
// scale: [1, 1.02, 0.98, 1]
|
|
// }}
|
|
// transition={{
|
|
// duration: 8,
|
|
// repeat: Infinity,
|
|
// ease: "easeInOut"
|
|
// }}
|
|
// >
|
|
// <ImageWithFallback
|
|
// src="https://images.unsplash.com/photo-1702825328124-dab63d85490e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx2aW50YWdlJTIwbG9nbyUyMHZlY3RvciUyMHBvc3RhbCUyMHN0YW1wfGVufDF8fHx8MTc1ODk5MjExN3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
// alt="Vintage logo"
|
|
// className="w-full h-full object-contain"
|
|
// style={{
|
|
// mixBlendMode: 'multiply',
|
|
// opacity: 0.8
|
|
// }}
|
|
// />
|
|
// {/* Subtle aging overlay for the vector */}
|
|
// <div
|
|
// className="absolute inset-0 rounded-full"
|
|
// style={{
|
|
// background: `
|
|
// radial-gradient(circle at 30% 30%, rgba(160, 130, 90, 0.2) 0%, transparent 60%),
|
|
// radial-gradient(circle at 70% 70%, rgba(140, 110, 70, 0.15) 0%, transparent 50%)
|
|
// `,
|
|
// mixBlendMode: 'multiply'
|
|
// }}
|
|
// />
|
|
// </motion.div>
|
|
|
|
// {/* For Correspondence Text */}
|
|
// <div
|
|
// className="absolute pointer-events-none"
|
|
// style={{
|
|
// position: 'absolute',
|
|
// width: '33.3%',
|
|
// height: '72.3%',
|
|
// left: '4.9%',
|
|
// top: '7.4%',
|
|
// display: 'flex',
|
|
// alignItems: 'flex-start',
|
|
// justifyContent: 'flex-start',
|
|
// paddingTop: '8%'
|
|
// }}
|
|
// >
|
|
// <p
|
|
// className="font-poppins font-light"
|
|
// style={{
|
|
// fontSize: 'clamp(10px, 2.2vw, 16px)',
|
|
// color: 'rgba(101, 84, 63, 0.4)',
|
|
// letterSpacing: '0.5px',
|
|
// lineHeight: '1.4',
|
|
// textShadow: '0px 0.5px 1px rgba(0, 0, 0, 0.05)'
|
|
// }}
|
|
// >
|
|
// For correspondence
|
|
// </p>
|
|
// </div>
|
|
|
|
// {/* Realistic vertical divider with ink bleeding - Responsive */}
|
|
// <div
|
|
// className="absolute"
|
|
// style={{
|
|
// top: '10.6%',
|
|
// bottom: '10.6%',
|
|
// left: '43.1%',
|
|
// width: '2px',
|
|
// background: `
|
|
// linear-gradient(to bottom,
|
|
// transparent 0%,
|
|
// rgba(101, 84, 63, 0.4) 10%,
|
|
// rgba(101, 84, 63, 0.6) 30%,
|
|
// rgba(101, 84, 63, 0.8) 50%,
|
|
// rgba(101, 84, 63, 0.6) 70%,
|
|
// rgba(101, 84, 63, 0.4) 90%,
|
|
// transparent 100%
|
|
// )
|
|
// `,
|
|
// filter: 'blur(0.3px)'
|
|
// }}
|
|
// />
|
|
|
|
// {/* Ink bleed effect around divider - Responsive */}
|
|
// <div
|
|
// className="absolute opacity-20"
|
|
// style={{
|
|
// top: '10.6%',
|
|
// bottom: '10.6%',
|
|
// left: '42.8%',
|
|
// width: '6px',
|
|
// background: `
|
|
// linear-gradient(to bottom,
|
|
// transparent 0%,
|
|
// rgba(101, 84, 63, 0.1) 20%,
|
|
// rgba(101, 84, 63, 0.15) 50%,
|
|
// rgba(101, 84, 63, 0.1) 80%,
|
|
// transparent 100%
|
|
// )
|
|
// `,
|
|
// filter: 'blur(1px)'
|
|
// }}
|
|
// />
|
|
|
|
// {/* Right side content area - Responsive */}
|
|
// <div
|
|
// className="absolute pointer-events-none"
|
|
// style={{
|
|
// top: '10.6%',
|
|
// left: '47.2%',
|
|
// width: '47.2%',
|
|
// height: '78.7%'
|
|
// }}
|
|
// >
|
|
// {/* Editable Address Label Card - Responsive */}
|
|
// <EditableCard
|
|
// isEditing={editingCard === 'label'}
|
|
// onEdit={() => setEditingCard(editingCard === 'label' ? null : 'label')}
|
|
// style={{
|
|
// position: 'absolute',
|
|
// top: '4.1%',
|
|
// left: '0%',
|
|
// pointerEvents: 'auto',
|
|
// transform: 'rotate(-0.3deg)'
|
|
// }}
|
|
// editIcon={<Type className="w-3 h-3 text-gray-600" />}
|
|
// >
|
|
// <div
|
|
// className="px-2 py-1 rounded text-xs md:text-sm"
|
|
// style={{
|
|
// fontWeight: '600',
|
|
// letterSpacing: '2px',
|
|
// textTransform: 'uppercase',
|
|
// color: 'rgba(101, 84, 63, 0.8)',
|
|
// textShadow: '0px 1px 2px rgba(0, 0, 0, 0.1)'
|
|
// }}
|
|
// >
|
|
// {postcardData.addressLabel}
|
|
// </div>
|
|
// </EditableCard>
|
|
|
|
// {/* Realistic horizontal ruled lines with ink bleeding - Responsive */}
|
|
// <div
|
|
// className="absolute pointer-events-none"
|
|
// style={{
|
|
// top: '14.9%',
|
|
// left: '0%',
|
|
// width: '94.1%',
|
|
// height: '59.5%'
|
|
// }}
|
|
// >
|
|
// {[...Array(9)].map((_, i) => (
|
|
// <div key={i} className="relative" style={{ marginBottom: '6.5%' }}>
|
|
// {/* Main line */}
|
|
// <div
|
|
// style={{
|
|
// height: '1px',
|
|
// width: i % 2 === 0 ? '100%' : '92%',
|
|
// background: `linear-gradient(to right,
|
|
// rgba(101, 84, 63, 0.3) 0%,
|
|
// rgba(101, 84, 63, 0.5) 20%,
|
|
// rgba(101, 84, 63, 0.6) 50%,
|
|
// rgba(101, 84, 63, 0.4) 80%,
|
|
// rgba(101, 84, 63, 0.2) 95%,
|
|
// transparent 100%
|
|
// )`,
|
|
// transform: `rotate(${(Math.random() - 0.5) * 0.5}deg)`
|
|
// }}
|
|
// />
|
|
// {/* Ink bleed effect */}
|
|
// <div
|
|
// className="absolute top-0 left-0 opacity-30"
|
|
// style={{
|
|
// height: '3px',
|
|
// width: i % 2 === 0 ? '98%' : '90%',
|
|
// background: `linear-gradient(to right,
|
|
// rgba(101, 84, 63, 0.1) 0%,
|
|
// rgba(101, 84, 63, 0.2) 30%,
|
|
// rgba(101, 84, 63, 0.15) 70%,
|
|
// transparent 100%
|
|
// )`,
|
|
// filter: 'blur(1px)',
|
|
// transform: 'translateY(-1px)'
|
|
// }}
|
|
// />
|
|
// </div>
|
|
// ))}
|
|
// </div>
|
|
|
|
// {/* Editable Message Card with Handwritten Animation - Responsive */}
|
|
// <EditableCard
|
|
// isEditing={editingCard === 'message'}
|
|
// onEdit={() => setEditingCard(editingCard === 'message' ? null : 'message')}
|
|
// style={{
|
|
// position: 'absolute',
|
|
// top: '17.6%',
|
|
// left: '4.4%',
|
|
// width: '88.2%',
|
|
// pointerEvents: 'auto',
|
|
// transform: 'rotate(-0.7deg)'
|
|
// }}
|
|
// editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
|
|
// >
|
|
// <div className="px-2 py-2 rounded leading-relaxed">
|
|
// {editingCard === 'message' ? (
|
|
// // Show static text when editing
|
|
// <div
|
|
// style={{
|
|
// fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
|
// fontSize: 'clamp(14px, 3.6vw, 26px)',
|
|
// lineHeight: '1.7',
|
|
// color: 'rgba(85, 70, 50, 0.9)',
|
|
// textShadow: `
|
|
// 1px 1px 2px rgba(0, 0, 0, 0.08),
|
|
// 0px 0px 3px rgba(101, 84, 63, 0.1)
|
|
// `,
|
|
// whiteSpace: 'pre-line',
|
|
// filter: 'contrast(110%) brightness(98%)'
|
|
// }}
|
|
// >
|
|
// {postcardData.message}
|
|
// </div>
|
|
// ) : (
|
|
// // Show animated handwritten text when not editing
|
|
// <HandwrittenText
|
|
// text={postcardData.message}
|
|
// speed={6}
|
|
// startDelay={0}
|
|
// autoStart={false}
|
|
// onComplete={handwrittenControl.onComplete}
|
|
// style={{
|
|
// fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
|
// fontSize: 'clamp(14px, 3.6vw, 26px)',
|
|
// lineHeight: '1.7',
|
|
// color: 'rgba(85, 70, 50, 0.9)',
|
|
// textShadow: `
|
|
// 1px 1px 2px rgba(0, 0, 0, 0.08),
|
|
// 0px 0px 3px rgba(101, 84, 63, 0.1)
|
|
// `,
|
|
// filter: 'contrast(110%) brightness(98%)'
|
|
// }}
|
|
// />
|
|
// )}
|
|
// </div>
|
|
// </EditableCard>
|
|
|
|
// {/* Editable Date Card - Responsive */}
|
|
// <EditableCard
|
|
// isEditing={editingCard === 'date'}
|
|
// onEdit={() => setEditingCard(editingCard === 'date' ? null : 'date')}
|
|
// style={{
|
|
// position: 'absolute',
|
|
// bottom: '16.2%',
|
|
// right: '7.4%',
|
|
// pointerEvents: 'auto',
|
|
// transform: 'rotate(-1.5deg)'
|
|
// }}
|
|
// editIcon={<Calendar className="w-3 h-3 text-gray-600" />}
|
|
// >
|
|
// <div
|
|
// className="px-2 py-1 rounded text-xs"
|
|
// style={{
|
|
// fontWeight: '500',
|
|
// letterSpacing: '1px',
|
|
// color: 'rgba(101, 84, 63, 0.7)',
|
|
// textShadow: '0px 1px 1px rgba(0, 0, 0, 0.05)',
|
|
// fontFamily: "'Poppins', sans-serif"
|
|
// }}
|
|
// >
|
|
// {postcardData.date}
|
|
// </div>
|
|
// </EditableCard>
|
|
// </div>
|
|
|
|
// {/* Ultra-realistic stamp - Responsive */}
|
|
// <EditableCard
|
|
// isEditing={editingCard === 'stamp'}
|
|
// onEdit={() => setEditingCard(editingCard === 'stamp' ? null : 'stamp')}
|
|
// style={{
|
|
// position: 'absolute',
|
|
// width: '12.5%',
|
|
// height: '19.1%',
|
|
// right: '3.5%',
|
|
// bottom: '5.3%',
|
|
// transform: 'rotate(-12deg)'
|
|
// }}
|
|
// editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
|
|
// >
|
|
// <div
|
|
// className="w-full h-full border-2 border-dashed rounded-full flex items-center justify-center relative"
|
|
// style={{
|
|
// borderColor: 'rgba(139, 115, 85, 0.8)',
|
|
// background: `
|
|
// radial-gradient(circle at 30% 30%, rgba(220, 190, 150, 0.95) 0%, rgba(200, 170, 130, 0.9) 40%, rgba(180, 150, 110, 0.85) 100%),
|
|
// linear-gradient(135deg, rgba(240, 220, 180, 0.3) 0%, transparent 50%)
|
|
// `,
|
|
// boxShadow: `
|
|
// inset 0px 2px 8px rgba(0, 0, 0, 0.25),
|
|
// inset 0px -1px 4px rgba(255, 255, 255, 0.2),
|
|
// 0px 4px 12px rgba(0, 0, 0, 0.15)
|
|
// `
|
|
// }}
|
|
// >
|
|
// {/* Stamp aging and wear */}
|
|
// <div
|
|
// className="absolute inset-0 rounded-full pointer-events-none"
|
|
// style={{
|
|
// background: `
|
|
// radial-gradient(circle at 70% 20%, rgba(160, 130, 90, 0.3) 0%, transparent 40%),
|
|
// radial-gradient(circle at 20% 80%, rgba(140, 110, 70, 0.2) 0%, transparent 35%)
|
|
// `,
|
|
// mixBlendMode: 'multiply'
|
|
// }}
|
|
// />
|
|
|
|
// {/* Stamp inner details */}
|
|
// <div className="absolute inset-3 border border-amber-800/50 rounded-full" />
|
|
// <div className="absolute inset-5 border border-amber-800/30 rounded-full" />
|
|
|
|
// {/* Stamp text */}
|
|
// <div className="text-center leading-tight z-10" style={{ color: 'rgba(101, 84, 63, 0.9)' }}>
|
|
// <div style={{ fontSize: 'clamp(6px, 1.25vw, 9px)', fontWeight: '700', letterSpacing: '1px' }}>
|
|
// TRAVEL
|
|
// </div>
|
|
// <div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '600', marginTop: '2px', letterSpacing: '0.5px' }}>
|
|
// MEMORIES
|
|
// </div>
|
|
// <div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '500', marginTop: '1px' }}>
|
|
// 2024
|
|
// </div>
|
|
// </div>
|
|
|
|
// {/* Stamp perforations with realistic variations */}
|
|
// {[...Array(20)].map((_, i) => (
|
|
// <div
|
|
// key={i}
|
|
// className="absolute rounded-full"
|
|
// style={{
|
|
// width: '2px',
|
|
// height: '2px',
|
|
// backgroundColor: 'rgba(101, 84, 63, 0.4)',
|
|
// top: '50%',
|
|
// left: '50%',
|
|
// transform: `translate(-50%, -50%) rotate(${i * 18}deg) translateY(-42px)`,
|
|
// opacity: Math.random() > 0.1 ? 1 : 0.3 // Random missing perforations
|
|
// }}
|
|
// />
|
|
// ))}
|
|
|
|
// {/* Stamp smudge mark */}
|
|
// <div
|
|
// className="absolute w-3 h-2 opacity-25"
|
|
// style={{
|
|
// background: 'radial-gradient(ellipse, rgba(101, 84, 63, 0.4) 0%, transparent 70%)',
|
|
// bottom: '15%',
|
|
// right: '20%',
|
|
// transform: 'rotate(20deg)',
|
|
// filter: 'blur(0.5px)'
|
|
// }}
|
|
// />
|
|
// </div>
|
|
// </EditableCard>
|
|
|
|
// {/* Additional realistic aging effects */}
|
|
// <div
|
|
// className="absolute w-3 h-1 opacity-15"
|
|
// style={{
|
|
// background: 'linear-gradient(45deg, rgba(120, 100, 70, 0.3), transparent)',
|
|
// top: '20%',
|
|
// left: '60%',
|
|
// transform: 'rotate(45deg)',
|
|
// filter: 'blur(0.5px)'
|
|
// }}
|
|
// />
|
|
// </motion.div>
|
|
// );
|
|
// };
|
|
|
|
return (
|
|
<section ref={sectionRef} className="py-12 md:py-16 lg:py-20 bg-white relative overflow-hidden">
|
|
{/* Background decorations */}
|
|
<div className="absolute inset-0 opacity-10 overflow-hidden">
|
|
{/* Vintage Stamps */}
|
|
<div className="absolute top-20 left-20 w-16 h-20 bg-warm-coral/30 rounded-sm rotate-12 border-2 border-warm-coral/20"></div>
|
|
<div className="absolute top-40 right-32 w-12 h-16 bg-warm-coral/30 rounded-sm -rotate-6 border-2 border-warm-coral/20"></div>
|
|
<div className="absolute bottom-32 left-40 w-14 h-18 bg-warm-coral/20 rounded-sm rotate-45 border-2 border-warm-coral/15"></div>
|
|
|
|
{/* Paper Textures */}
|
|
<div className="absolute top-1/3 right-1/4 w-32 h-32 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
|
<div className="absolute bottom-1/3 left-1/4 w-40 h-40 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
|
|
|
{/* Ink Splatters */}
|
|
<div className="absolute top-1/2 left-1/2 w-8 h-8 bg-warm-coral/20 rounded-full blur-sm"></div>
|
|
<div className="absolute top-1/4 right-1/3 w-6 h-6 bg-warm-coral/20 rounded-full blur-sm"></div>
|
|
<div className="absolute bottom-1/4 left-1/3 w-4 h-4 bg-warm-coral/15 rounded-full blur-sm"></div>
|
|
</div>
|
|
|
|
<div className="container mx-auto px-4 relative z-10">
|
|
{/* Header */}
|
|
<div className="text-center mb-12 md:mb-16">
|
|
<div className="inline-flex items-center gap-2 bg-gray-50 px-4 py-2 rounded-full mb-4">
|
|
<Camera className="w-4 h-4 text-warm-coral" />
|
|
<span className="text-sm text-gray-700">Custom Memories</span>
|
|
</div>
|
|
|
|
<h2 className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl text-gray-900 mb-6">
|
|
<span className="font-light">The Only Card That Sends Your</span>
|
|
<span className="block">
|
|
<span className="font-bold text-warm-coral italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-2">
|
|
Holiday
|
|
</span>{' '}
|
|
<span className="font-normal">Home.</span>
|
|
</span>
|
|
</h2>
|
|
|
|
<p className="text-lg md:text-xl text-gray-600 leading-relaxed max-w-3xl mx-auto">
|
|
Transform your travel memories into beautiful, personalized postcards that capture the essence of your adventures.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Centered Postcard Preview - Enhanced with Animations */}
|
|
<div className="flex justify-center mb-12 px-4">
|
|
<motion.div
|
|
className="relative group w-full max-w-4xl [perspective:2000px] flex items-center gap-6"
|
|
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
transition={{
|
|
duration: 0.6,
|
|
ease: [0.25, 0.1, 0.25, 1],
|
|
delay: 0.2
|
|
}}
|
|
>
|
|
{/* Left Flip Button - Show when viewing back (flipped) */}
|
|
<motion.button
|
|
onClick={() => setIsFlipped(false)}
|
|
className={`hidden md:flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-lg border-2 border-primary hover:bg-primary hover:text-white transition-all duration-300 ${isFlipped ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: isFlipped ? 1 : 0, x: isFlipped ? 0 : -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<ArrowRight className="w-5 h-5 rotate-180" />
|
|
</motion.button>
|
|
|
|
{/* Postcard Container */}
|
|
<div className="relative flex-1">
|
|
{/* Interactive 3D Container with Flip */}
|
|
<motion.div
|
|
ref={postcardRef}
|
|
className="relative w-full rounded-xl cursor-pointer [transform-style:preserve-3d]"
|
|
style={{
|
|
aspectRatio: '720 / 470',
|
|
maxWidth: '720px',
|
|
maxHeight: '470px',
|
|
minWidth: '280px',
|
|
minHeight: '183px',
|
|
rotateX,
|
|
rotateY,
|
|
transformStyle: "preserve-3d"
|
|
}}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
whileHover={{
|
|
scale: 1.02,
|
|
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(99, 102, 241, 0.1)"
|
|
}}
|
|
animate={{
|
|
y: [0, -8, 0],
|
|
rotateY: isFlipped ? 180 : 0
|
|
}}
|
|
transition={{
|
|
y: {
|
|
duration: 4,
|
|
repeat: Infinity,
|
|
ease: "easeInOut"
|
|
},
|
|
rotateY: {
|
|
duration: 0.4,
|
|
ease: [0.25, 1, 0.5, 1]
|
|
}
|
|
}}
|
|
>
|
|
{/* Front Side - Postcard Design */}
|
|
<motion.div
|
|
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
|
|
style={{
|
|
backfaceVisibility: "hidden",
|
|
WebkitBackfaceVisibility: "hidden"
|
|
}}
|
|
>
|
|
{/* <PostcardFrame /> */}
|
|
<img src={front} alt="Postcard Image" />
|
|
|
|
{/* Subtle glow effect */}
|
|
<motion.div
|
|
className="absolute inset-0 rounded-xl opacity-0 pointer-events-none"
|
|
style={{
|
|
background: "radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%)",
|
|
filter: "blur(20px)"
|
|
}}
|
|
whileHover={{ opacity: 0.5 }}
|
|
transition={{ duration: 0.3 }}
|
|
/>
|
|
</motion.div>
|
|
|
|
{/* Back Side - Postcard Frame with Upload */}
|
|
<motion.div
|
|
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden] group/image"
|
|
style={{
|
|
backfaceVisibility: "hidden",
|
|
WebkitBackfaceVisibility: "hidden",
|
|
transform: "rotateY(180deg)"
|
|
}}
|
|
>
|
|
{/* Postcard frame with gradient background - matching Figma import */}
|
|
<div className="bg-gradient-to-r from-[#e2d6c2] to-[#ffffff] via-50% via-[#fff5e6] relative rounded-xl size-full">
|
|
<div className="flex flex-col items-center size-full">
|
|
<div className="box-border content-stretch flex flex-col gap-[4px] items-center overflow-clip px-[12px] py-[8px] relative size-full">
|
|
<div className="basis-0 grow min-h-px min-w-px relative rounded-[2px] shrink-0 w-full">
|
|
<div aria-hidden="true" className="absolute inset-0 pointer-events-none rounded-[2px]">
|
|
<div className="absolute bg-[#d9d9d9] inset-0 rounded-[2px]" />
|
|
<div className="absolute inset-0 overflow-hidden rounded-[2px]">
|
|
{uploadedImage ? (
|
|
<img
|
|
alt="Your uploaded postcard"
|
|
className="absolute h-full left-0 max-w-none top-0 w-full object-cover"
|
|
src={uploadedImage}
|
|
/>
|
|
) : (
|
|
<img
|
|
src="https://images.unsplash.com/photo-1631786517313-52f119448c17?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZXxlbnwxfHx8fDE3NjIxNzExNzF8MA&ixlib=rb-4.1.0&q=80&w=1080"
|
|
alt="Melbourne skyline"
|
|
className="absolute h-full left-0 max-w-none top-0 w-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div aria-hidden="true" className="absolute border border-[rgba(0,0,0,0.12)] border-solid inset-0 pointer-events-none rounded-xl" />
|
|
|
|
{/* Upload/Edit Button - appears on hover */}
|
|
<motion.div
|
|
className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 rounded-xl"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
triggerFileInput();
|
|
}}
|
|
>
|
|
<motion.button
|
|
className="bg-white hover:bg-primary text-gray-800 hover:text-white px-6 py-3 rounded-full shadow-lg transition-all duration-300 flex items-center gap-2 font-poppins font-semibold"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Upload className="w-5 h-5" />
|
|
<span>{uploadedImage ? 'Change Image' : 'Upload Image'}</span>
|
|
</motion.button>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Hidden file input */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleImageUpload}
|
|
className="hidden"
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
{/* Animated Edit Instructions */}
|
|
{editingCard && (
|
|
<motion.div
|
|
className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-primary text-white px-3 py-2 md:px-4 md:py-2 rounded-lg shadow-lg text-xs md:text-sm font-medium whitespace-nowrap z-20"
|
|
initial={{ opacity: 0, y: 10, scale: 0.9 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: -10, scale: 0.9 }}
|
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
>
|
|
<motion.span
|
|
animate={{
|
|
color: ["#ffffff", "#e0e7ff", "#ffffff"]
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut"
|
|
}}
|
|
>
|
|
Click on any element to edit it
|
|
</motion.span>
|
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Mobile Flip Button - Below postcard on mobile */}
|
|
<motion.button
|
|
onClick={() => setIsFlipped(!isFlipped)}
|
|
className="md:hidden mt-4 mx-auto flex items-center justify-center gap-2 px-6 py-3 rounded-full bg-primary text-white font-poppins font-semibold shadow-lg hover:bg-primary/90 transition-all duration-300"
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Camera className="w-4 h-4" />
|
|
<span>{isFlipped ? 'View Postcard' : 'View Image'}</span>
|
|
</motion.button>
|
|
</div>
|
|
|
|
{/* Right Flip Button - Show when viewing front (not flipped) */}
|
|
<motion.button
|
|
onClick={() => setIsFlipped(true)}
|
|
className={`hidden md:flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-lg border-2 border-primary hover:bg-primary hover:text-white transition-all duration-300 ${!isFlipped ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: !isFlipped ? 1 : 0, x: !isFlipped ? 0 : 20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<ArrowRight className="w-5 h-5" />
|
|
</motion.button>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Animated Editing Panel */}
|
|
{editingCard && (
|
|
<motion.div
|
|
className="max-w-md mx-auto mb-12 p-6 bg-gray-50 rounded-lg border"
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
>
|
|
<h3 className="font-medium mb-4 capitalize">Edit {editingCard}</h3>
|
|
{editingCard === 'photo' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Photo URL</label>
|
|
<input
|
|
type="text"
|
|
value={postcardData.photo}
|
|
onChange={(e) => setPostcardData(prev => ({ ...prev, photo: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
placeholder="Enter image URL"
|
|
/>
|
|
</div>
|
|
)}
|
|
{editingCard === 'message' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Message</label>
|
|
<textarea
|
|
value={postcardData.message}
|
|
onChange={(e) => setPostcardData(prev => ({ ...prev, message: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary h-32 resize-none"
|
|
placeholder="Enter your message"
|
|
/>
|
|
</div>
|
|
)}
|
|
{editingCard === 'date' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Date Text</label>
|
|
<input
|
|
type="text"
|
|
value={postcardData.date}
|
|
onChange={(e) => setPostcardData(prev => ({ ...prev, date: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
placeholder="Enter date text"
|
|
/>
|
|
</div>
|
|
)}
|
|
{editingCard === 'label' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Address Label</label>
|
|
<input
|
|
type="text"
|
|
value={postcardData.addressLabel}
|
|
onChange={(e) => setPostcardData(prev => ({ ...prev, addressLabel: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
placeholder="Enter label text"
|
|
/>
|
|
</div>
|
|
)}
|
|
{editingCard === 'stamp' && (
|
|
<div>
|
|
<p className="text-sm text-gray-600">Stamp design features authentic vintage styling with realistic aging effects.</p>
|
|
</div>
|
|
)}
|
|
<div className="flex gap-2 mt-4">
|
|
<motion.div
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Button
|
|
onClick={() => setEditingCard(null)}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1"
|
|
>
|
|
Done
|
|
</Button>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Enhanced Call to Action */}
|
|
<div className="text-center">
|
|
<Button
|
|
onClick={handleCreatePostcard}
|
|
className="bg-[#f95f62] hover:bg-warm-coral/90 text-white px-8 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 mx-auto"
|
|
>
|
|
Customize Your Postcard
|
|
<ArrowRight className="w-5 h-5 ml-2" />
|
|
</Button>
|
|
<motion.p
|
|
className="text-gray-600 mt-6"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5, delay: 0.6 }}
|
|
>
|
|
Click on any postcard element above to edit it, or create a completely new design
|
|
</motion.p>
|
|
</div>
|
|
|
|
{/* Enhanced Features Section */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-10 mt-12">
|
|
{[
|
|
{
|
|
icon: Stamp,
|
|
title: "Authentic Design",
|
|
description: "Realistic vintage styling with aging effect, paper texture, and authentic details"
|
|
},
|
|
{
|
|
icon: Edit,
|
|
title: "Handwritten Style",
|
|
description: "Beautiful cursive fonts with realistic ink bleeding and natural imperfections"
|
|
},
|
|
{
|
|
icon: Camera,
|
|
title: "Easy Customization",
|
|
description: "Click any element to edit photos, messages, and details in real-time"
|
|
}
|
|
].map((feature, index) => (
|
|
<motion.div
|
|
key={index}
|
|
className="text-center group p-6 rounded-xl bg-white hover:bg-gray-50 transition-all duration-300 border border-gray-100 hover:border-warm-coral/20 hover:shadow-lg"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
|
whileHover={{
|
|
y: -5,
|
|
transition: { duration: 0.2 }
|
|
}}
|
|
>
|
|
<motion.div
|
|
className="mx-auto mb-4 p-3 rounded-full bg-warm-coral/10 group-hover:bg-warm-coral/20 transition-all duration-300 w-16 h-16 flex items-center justify-center"
|
|
whileHover={{
|
|
scale: 1.1,
|
|
rotate: 360,
|
|
transition: { duration: 0.5 }
|
|
}}
|
|
>
|
|
<feature.icon className="w-8 h-8 text-warm-coral" />
|
|
</motion.div>
|
|
<h3 className="mb-2 text-gray-900 group-hover:text-warm-coral transition-colors duration-300">
|
|
{feature.title}
|
|
</h3>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
{feature.description}
|
|
</p>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |