refactor: Cleaner landing page - focused hero & reduced animations
**User Feedback:** - Hero Section: Too much going on, redundant generator - Animations: Too excessive throughout - Phone Mockup: Works great, keep it! **Hero Section - Major Cleanup:** - REMOVED: Interactive QR Generator (redundant with generator below) - NEW: QRTypesShowcase - 3x3 grid showing 9 QR code types - NEW: Auto-rotating phone mockup demonstrating each type - Shows variety instead of single interactive element - Much cleaner, more focused first impression **Animation Cleanup:** - FeatureCustomizationDemo: Cycles ONCE then stops - FeatureBulkDemo: Animates ONCE then stays static - Features.tsx: Removed all infinite animations (rotate, scale, etc.) - StatsCounter: Subtiler - smaller text, slower animation - No more animation overload! **Philosophy:** - CLEANER > overloaded - FOCUSED > excessive interaction - SUBTLE > flashy animations - Show variety > show everything **PhoneMockup Enhanced:** - Auto-rotates through 9 QR types every 5s - Shows scan animation for each type - Displays type name in notification - Clean demo of all capabilities **Components:** - NEW: QRTypesShowcase.tsx - Grid with 9 QR types - UPDATED: PhoneMockup.tsx - Auto-rotation logic - UPDATED: Hero.tsx - Uses showcase instead of generator - UPDATED: Features.tsx - Static icons, no infinite loops - UPDATED: StatsCounter.tsx - Subtiler appearance Result: Professional, clean, focused landing page without animation chaos! Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a7180e3b9b
commit
3c6d75b6bb
|
|
@ -7,14 +7,18 @@ import { FileSpreadsheet, ArrowRight } from 'lucide-react';
|
|||
|
||||
export const FeatureBulkDemo: React.FC = () => {
|
||||
const [showQRs, setShowQRs] = useState(false);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setShowQRs((prev) => !prev);
|
||||
}, 4000);
|
||||
if (hasAnimated) return;
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
const timer = setTimeout(() => {
|
||||
setShowQRs(true);
|
||||
setHasAnimated(true);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [hasAnimated]);
|
||||
|
||||
const qrCodes = [
|
||||
{ id: 1, url: 'product-1', delay: 0 },
|
||||
|
|
|
|||
|
|
@ -14,14 +14,25 @@ const colorPresets = [
|
|||
|
||||
export const FeatureCustomizationDemo: React.FC = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAnimated) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setActiveIndex((prev) => (prev + 1) % colorPresets.length);
|
||||
}, 2500);
|
||||
setActiveIndex((prev) => {
|
||||
const next = prev + 1;
|
||||
if (next >= colorPresets.length) {
|
||||
clearInterval(interval);
|
||||
setHasAnimated(true);
|
||||
return 0; // Reset to first
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}, [hasAnimated]);
|
||||
|
||||
const currentPreset = colorPresets[activeIndex];
|
||||
|
||||
|
|
|
|||
|
|
@ -61,19 +61,9 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
{t.features.unlimited.description}
|
||||
</p>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: 360,
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: 'linear'
|
||||
}}
|
||||
className="self-end"
|
||||
>
|
||||
<div className="self-end">
|
||||
<InfinityIcon className="w-16 h-16 text-indigo-200" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</BentoItem>
|
||||
|
||||
|
|
@ -89,21 +79,11 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
99.9% uptime guarantee with enterprise-grade infrastructure
|
||||
</p>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut'
|
||||
}}
|
||||
className="self-end"
|
||||
>
|
||||
<div className="self-end">
|
||||
<div className="text-4xl font-bold text-green-600">
|
||||
99.9<span className="text-2xl">%</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</BentoItem>
|
||||
|
||||
|
|
@ -119,19 +99,9 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
Bank-level encryption and GDPR compliant data handling
|
||||
</p>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotateY: [0, 180, 360],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut'
|
||||
}}
|
||||
className="self-end"
|
||||
>
|
||||
<div className="self-end">
|
||||
<Shield className="w-16 h-16 text-amber-200" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</BentoItem>
|
||||
</BentoGrid>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { Badge } from '@/components/ui/Badge';
|
|||
import { motion } from 'framer-motion';
|
||||
import { CheckCircle2, ArrowRight, Zap } from 'lucide-react';
|
||||
import { AnimatedBackground } from './AnimatedBackground';
|
||||
import { HeroQRInteractive } from './HeroQRInteractive';
|
||||
import { QRTypesShowcase } from './QRTypesShowcase';
|
||||
import { PhoneMockup } from './PhoneMockup';
|
||||
import { StatsCounter, defaultStats } from './StatsCounter';
|
||||
import { slideUp, slideRight, staggerContainer, staggerItem } from '@/lib/animations';
|
||||
|
||||
interface HeroProps {
|
||||
t: any; // i18n translation function
|
||||
|
|
@ -26,13 +26,18 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
{/* Left: Content */}
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-8 text-center lg:text-left"
|
||||
>
|
||||
{/* Badge */}
|
||||
<motion.div variants={staggerItem} className="flex justify-center lg:justify-start">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.1, duration: 0.3 }}
|
||||
className="flex justify-center lg:justify-start"
|
||||
>
|
||||
<Badge variant="info" className="inline-flex items-center space-x-2 px-4 py-2 shadow-sm">
|
||||
<Zap className="w-4 h-4" />
|
||||
<span className="font-semibold">{t.hero.badge}</span>
|
||||
|
|
@ -40,7 +45,12 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</motion.div>
|
||||
|
||||
{/* Headline */}
|
||||
<motion.div variants={staggerItem} className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h2 className="text-5xl sm:text-6xl lg:text-7xl font-bold text-gray-900 leading-tight">
|
||||
{t.hero.title}
|
||||
</h2>
|
||||
|
|
@ -51,13 +61,18 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</motion.div>
|
||||
|
||||
{/* Feature List */}
|
||||
<motion.div variants={staggerItem} className="space-y-4 pt-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.4 }}
|
||||
className="space-y-4 pt-4"
|
||||
>
|
||||
{t.hero.features.map((feature: string, index: number) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4 + index * 0.1, duration: 0.4 }}
|
||||
transition={{ delay: 0.4 + index * 0.08, duration: 0.3 }}
|
||||
className="flex items-center space-x-3 justify-center lg:justify-start"
|
||||
>
|
||||
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-gradient-to-br from-emerald-400 to-emerald-600 flex items-center justify-center shadow-sm">
|
||||
|
|
@ -70,7 +85,9 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
|
||||
{/* CTAs */}
|
||||
<motion.div
|
||||
variants={staggerItem}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6, duration: 0.3 }}
|
||||
className="flex flex-col sm:flex-row gap-4 pt-6 justify-center lg:justify-start"
|
||||
>
|
||||
<Link href="/signup">
|
||||
|
|
@ -97,7 +114,7 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1, duration: 0.5 }}
|
||||
transition={{ delay: 0.8, duration: 0.3 }}
|
||||
className="flex items-center gap-2 text-sm text-gray-500 justify-center lg:justify-start"
|
||||
>
|
||||
<div className="flex -space-x-2">
|
||||
|
|
@ -112,19 +129,27 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right: Interactive QR Generator */}
|
||||
{/* Right: QR Types Showcase + Phone */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.6 }}
|
||||
className="relative"
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
className="relative space-y-8"
|
||||
>
|
||||
<HeroQRInteractive />
|
||||
{/* QR Types Grid */}
|
||||
<QRTypesShowcase />
|
||||
|
||||
{/* Phone Mockup with Auto-Rotation */}
|
||||
<div className="flex justify-center">
|
||||
<PhoneMockup autoRotate={true} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Stats Counter */}
|
||||
<StatsCounter stats={defaultStats} />
|
||||
{/* Stats Counter - Subtiler */}
|
||||
<div className="mt-20">
|
||||
<StatsCounter stats={defaultStats} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smooth Gradient Fade Transition to next section */}
|
||||
|
|
|
|||
|
|
@ -4,24 +4,37 @@ import React, { useEffect, useState } from 'react';
|
|||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Wifi, Battery, Signal } from 'lucide-react';
|
||||
import { qrTypes } from './QRTypesShowcase';
|
||||
|
||||
interface PhoneMockupProps {
|
||||
url: string;
|
||||
foreground: string;
|
||||
background: string;
|
||||
url?: string;
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
autoRotate?: boolean;
|
||||
}
|
||||
|
||||
export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
||||
url,
|
||||
foreground,
|
||||
background,
|
||||
foreground = '#000000',
|
||||
background = '#FFFFFF',
|
||||
autoRotate = false,
|
||||
}) => {
|
||||
const [currentTypeIndex, setCurrentTypeIndex] = useState(0);
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
|
||||
// Auto-scan animation every 6 seconds
|
||||
const currentType = autoRotate ? qrTypes[currentTypeIndex] : null;
|
||||
const displayUrl = url || (currentType ? currentType.data : 'https://qrmaster.net');
|
||||
const displayName = currentType ? currentType.name : 'QR Master';
|
||||
|
||||
// Auto-rotate through QR types
|
||||
useEffect(() => {
|
||||
if (!autoRotate) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTypeIndex((prev) => (prev + 1) % qrTypes.length);
|
||||
|
||||
// Trigger scan animation
|
||||
setIsScanning(true);
|
||||
setTimeout(() => {
|
||||
setIsScanning(false);
|
||||
|
|
@ -29,30 +42,32 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
|||
setTimeout(() => {
|
||||
setShowResult(false);
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
}, 6000);
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}, [autoRotate]);
|
||||
|
||||
// Extract domain from URL for preview
|
||||
const getDomain = (urlString: string) => {
|
||||
try {
|
||||
const urlObj = new URL(urlString);
|
||||
return urlObj.hostname.replace('www.', '');
|
||||
if (urlString.startsWith('http')) {
|
||||
const urlObj = new URL(urlString);
|
||||
return urlObj.hostname.replace('www.', '');
|
||||
}
|
||||
return urlString.substring(0, 20);
|
||||
} catch {
|
||||
return 'qrmaster.net';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[300px] mx-auto">
|
||||
<div className="relative w-full max-w-[280px] mx-auto">
|
||||
{/* Phone Frame */}
|
||||
<div className="relative">
|
||||
{/* Phone outline */}
|
||||
<div className="relative bg-gray-900 rounded-[3rem] p-3 shadow-2xl">
|
||||
<div className="relative bg-gray-900 rounded-[2.5rem] p-2.5 shadow-2xl">
|
||||
{/* Screen */}
|
||||
<div className="relative bg-white rounded-[2.5rem] overflow-hidden aspect-[9/19.5]">
|
||||
<div className="relative bg-white rounded-[2rem] overflow-hidden aspect-[9/19.5]">
|
||||
{/* Status Bar */}
|
||||
<div className="absolute top-0 left-0 right-0 z-20 px-6 pt-2 pb-1 bg-gradient-to-b from-black/5 to-transparent">
|
||||
<div className="flex justify-between items-center">
|
||||
|
|
@ -66,57 +81,59 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Camera Notch */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-6 bg-gray-900 rounded-b-2xl z-30" />
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-20 h-5 bg-gray-900 rounded-b-2xl z-30" />
|
||||
|
||||
{/* Screen Content */}
|
||||
<div className="relative h-full bg-gray-50 pt-8">
|
||||
<div className="px-4 h-full flex flex-col">
|
||||
{/* Camera App Header */}
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="font-semibold text-gray-900">QR Scanner</h3>
|
||||
<div className="text-center mb-3">
|
||||
<h3 className="font-semibold text-gray-900 text-sm">QR Scanner</h3>
|
||||
</div>
|
||||
|
||||
{/* QR Code Display Area */}
|
||||
<div className="flex-1 flex items-center justify-center relative">
|
||||
{/* QR Code */}
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: isScanning ? 0.95 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="p-4 bg-white rounded-2xl shadow-lg">
|
||||
<QRCodeSVG
|
||||
value={url || 'https://qrmaster.net'}
|
||||
size={140}
|
||||
fgColor={foreground}
|
||||
bgColor={background}
|
||||
level="M"
|
||||
/>
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={autoRotate ? currentTypeIndex : displayUrl}
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: isScanning ? 0.95 : 1, opacity: 1 }}
|
||||
exit={{ scale: 0.8, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="p-3 bg-white rounded-xl shadow-lg">
|
||||
<QRCodeSVG
|
||||
value={displayUrl}
|
||||
size={120}
|
||||
fgColor={foreground}
|
||||
bgColor={background}
|
||||
level="M"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scan Overlay */}
|
||||
<AnimatePresence>
|
||||
{isScanning && (
|
||||
<motion.div
|
||||
initial={{ y: -100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ duration: 1.5, ease: 'linear' }}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<div className="w-full h-1 bg-gradient-to-r from-transparent via-primary-500 to-transparent shadow-lg" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Scan Overlay */}
|
||||
<AnimatePresence>
|
||||
{isScanning && (
|
||||
<motion.div
|
||||
initial={{ y: -80, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 80, opacity: 0 }}
|
||||
transition={{ duration: 0.8, ease: 'linear' }}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<div className="w-full h-0.5 bg-gradient-to-r from-transparent via-primary-500 to-transparent shadow-lg" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Corner Brackets */}
|
||||
<div className="absolute top-2 left-2 w-6 h-6 border-t-2 border-l-2 border-primary-500 rounded-tl-lg" />
|
||||
<div className="absolute top-2 right-2 w-6 h-6 border-t-2 border-r-2 border-primary-500 rounded-tr-lg" />
|
||||
<div className="absolute bottom-2 left-2 w-6 h-6 border-b-2 border-l-2 border-primary-500 rounded-bl-lg" />
|
||||
<div className="absolute bottom-2 right-2 w-6 h-6 border-b-2 border-r-2 border-primary-500 rounded-br-lg" />
|
||||
</motion.div>
|
||||
{/* Corner Brackets */}
|
||||
<div className="absolute top-1 left-1 w-5 h-5 border-t-2 border-l-2 border-primary-500 rounded-tl-lg" />
|
||||
<div className="absolute top-1 right-1 w-5 h-5 border-t-2 border-r-2 border-primary-500 rounded-tr-lg" />
|
||||
<div className="absolute bottom-1 left-1 w-5 h-5 border-b-2 border-l-2 border-primary-500 rounded-bl-lg" />
|
||||
<div className="absolute bottom-1 right-1 w-5 h-5 border-b-2 border-r-2 border-primary-500 rounded-br-lg" />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Result Notification */}
|
||||
|
|
@ -126,17 +143,17 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
|||
initial={{ y: 100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
className="mb-4 p-4 bg-white rounded-2xl shadow-lg border border-gray-100"
|
||||
className="mb-3 p-3 bg-white rounded-xl shadow-lg border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-success-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-success-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-success-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-success-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-gray-900">QR Code Scanned</p>
|
||||
<p className="text-xs text-gray-500 truncate">{getDomain(url)}</p>
|
||||
<p className="text-xs font-semibold text-gray-900">{displayName}</p>
|
||||
<p className="text-[10px] text-gray-500 truncate">{getDomain(displayUrl)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -144,9 +161,9 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
|||
</AnimatePresence>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<div className="text-center pb-6">
|
||||
<p className="text-xs text-gray-500">
|
||||
{isScanning ? 'Scanning...' : 'Point camera at QR code'}
|
||||
<div className="text-center pb-4">
|
||||
<p className="text-[10px] text-gray-500">
|
||||
{isScanning ? 'Scanning...' : autoRotate ? 'Auto-scanning demo' : 'Point camera at QR code'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,24 +171,14 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Side buttons */}
|
||||
<div className="absolute right-0 top-20 w-1 h-12 bg-gray-800 rounded-r" />
|
||||
<div className="absolute right-0 top-36 w-1 h-8 bg-gray-800 rounded-r" />
|
||||
<div className="absolute left-0 top-24 w-1 h-16 bg-gray-800 rounded-l" />
|
||||
<div className="absolute right-0 top-16 w-0.5 h-10 bg-gray-800 rounded-r" />
|
||||
<div className="absolute right-0 top-28 w-0.5 h-6 bg-gray-800 rounded-r" />
|
||||
<div className="absolute left-0 top-20 w-0.5 h-12 bg-gray-800 rounded-l" />
|
||||
</div>
|
||||
|
||||
{/* Phone Shadow */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-black/20 to-black/40 rounded-[3rem] -z-10 blur-xl translate-y-4" />
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-black/20 to-black/40 rounded-[2.5rem] -z-10 blur-xl translate-y-3" />
|
||||
</div>
|
||||
|
||||
{/* Floating Label */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute -bottom-6 left-1/2 -translate-x-1/2 px-4 py-2 bg-gray-900 text-white text-xs font-medium rounded-full whitespace-nowrap shadow-lg"
|
||||
>
|
||||
Live Scan Preview
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Globe, User, Wifi, Mail, MessageSquare, Phone, MapPin, Calendar, FileText } from 'lucide-react';
|
||||
|
||||
export interface QRType {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
data: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const qrTypes: QRType[] = [
|
||||
{
|
||||
id: 'url',
|
||||
name: 'URL/Website',
|
||||
icon: Globe,
|
||||
data: 'https://qrmaster.net',
|
||||
color: 'text-blue-600 bg-blue-50'
|
||||
},
|
||||
{
|
||||
id: 'vcard',
|
||||
name: 'vCard',
|
||||
icon: User,
|
||||
data: 'BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nEND:VCARD',
|
||||
color: 'text-purple-600 bg-purple-50'
|
||||
},
|
||||
{
|
||||
id: 'wifi',
|
||||
name: 'WiFi',
|
||||
icon: Wifi,
|
||||
data: 'WIFI:T:WPA;S:MyNetwork;P:password;;',
|
||||
color: 'text-cyan-600 bg-cyan-50'
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
name: 'Email',
|
||||
icon: Mail,
|
||||
data: 'mailto:hello@qrmaster.net',
|
||||
color: 'text-red-600 bg-red-50'
|
||||
},
|
||||
{
|
||||
id: 'sms',
|
||||
name: 'SMS',
|
||||
icon: MessageSquare,
|
||||
data: 'SMSTO:+1234567890:Hello!',
|
||||
color: 'text-green-600 bg-green-50'
|
||||
},
|
||||
{
|
||||
id: 'phone',
|
||||
name: 'Phone',
|
||||
icon: Phone,
|
||||
data: 'tel:+1234567890',
|
||||
color: 'text-emerald-600 bg-emerald-50'
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
name: 'Location',
|
||||
icon: MapPin,
|
||||
data: 'geo:37.7749,-122.4194',
|
||||
color: 'text-pink-600 bg-pink-50'
|
||||
},
|
||||
{
|
||||
id: 'event',
|
||||
name: 'Event',
|
||||
icon: Calendar,
|
||||
data: 'BEGIN:VEVENT\nSUMMARY:Meeting\nEND:VEVENT',
|
||||
color: 'text-indigo-600 bg-indigo-50'
|
||||
},
|
||||
{
|
||||
id: 'menu',
|
||||
name: 'Menu/PDF',
|
||||
icon: FileText,
|
||||
data: 'https://qrmaster.net/menu.pdf',
|
||||
color: 'text-orange-600 bg-orange-50'
|
||||
}
|
||||
];
|
||||
|
||||
interface QRTypesShowcaseProps {
|
||||
onTypeSelect?: (type: QRType) => void;
|
||||
}
|
||||
|
||||
export const QRTypesShowcase: React.FC<QRTypesShowcaseProps> = ({ onTypeSelect }) => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Grid of QR Types */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{qrTypes.map((type, index) => (
|
||||
<motion.button
|
||||
key={type.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.05,
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.1, 0.25, 1]
|
||||
}}
|
||||
onClick={() => onTypeSelect?.(type)}
|
||||
className="group relative p-4 bg-white rounded-2xl border-2 border-gray-100 hover:border-primary-200 hover:shadow-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{/* Icon Badge */}
|
||||
<div className={`w-10 h-10 ${type.color} rounded-xl flex items-center justify-center mb-3 mx-auto transition-transform group-hover:scale-110`}>
|
||||
<type.icon className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
{/* QR Code */}
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="p-2 bg-white rounded-lg">
|
||||
<QRCodeSVG
|
||||
value={type.data}
|
||||
size={60}
|
||||
level="L"
|
||||
fgColor="#000000"
|
||||
bgColor="#FFFFFF"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<p className="text-xs font-semibold text-gray-700 text-center group-hover:text-primary-600 transition-colors">
|
||||
{type.name}
|
||||
</p>
|
||||
|
||||
{/* Subtle glow on hover */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-primary opacity-0 group-hover:opacity-5 transition-opacity pointer-events-none" />
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Caption */}
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Support for all major QR code types
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { qrTypes };
|
||||
|
|
@ -25,8 +25,8 @@ const AnimatedNumber: React.FC<{
|
|||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
const motionValue = useMotionValue(0);
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping: 60,
|
||||
stiffness: 100,
|
||||
damping: 80,
|
||||
stiffness: 60,
|
||||
});
|
||||
const [displayValue, setDisplayValue] = React.useState('0');
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ const AnimatedNumber: React.FC<{
|
|||
}, [springValue, decimals]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className="gradient-text-vibrant text-4xl lg:text-5xl font-bold">
|
||||
<span ref={ref} className="gradient-text-vibrant text-3xl lg:text-4xl font-bold">
|
||||
{prefix}
|
||||
{displayValue}
|
||||
{suffix}
|
||||
|
|
@ -57,16 +57,18 @@ export const StatsCounter: React.FC<StatsCounterProps> = ({ stats }) => {
|
|||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8, duration: 0.5 }}
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-8 mt-16"
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-6 mt-16"
|
||||
>
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.9 + index * 0.1, duration: 0.4 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1, duration: 0.3 }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="mb-2">
|
||||
|
|
@ -77,7 +79,7 @@ export const StatsCounter: React.FC<StatsCounterProps> = ({ stats }) => {
|
|||
decimals={stat.decimals}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-600">{stat.label}</p>
|
||||
<p className="text-xs font-medium text-gray-600">{stat.label}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue