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:
Timo 2026-01-19 09:14:52 +01:00
parent a7180e3b9b
commit 3c6d75b6bb
7 changed files with 311 additions and 149 deletions

View File

@ -7,14 +7,18 @@ import { FileSpreadsheet, ArrowRight } from 'lucide-react';
export const FeatureBulkDemo: React.FC = () => { export const FeatureBulkDemo: React.FC = () => {
const [showQRs, setShowQRs] = useState(false); const [showQRs, setShowQRs] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { if (hasAnimated) return;
setShowQRs((prev) => !prev);
}, 4000);
return () => clearInterval(interval); const timer = setTimeout(() => {
}, []); setShowQRs(true);
setHasAnimated(true);
}, 500);
return () => clearTimeout(timer);
}, [hasAnimated]);
const qrCodes = [ const qrCodes = [
{ id: 1, url: 'product-1', delay: 0 }, { id: 1, url: 'product-1', delay: 0 },

View File

@ -14,14 +14,25 @@ const colorPresets = [
export const FeatureCustomizationDemo: React.FC = () => { export const FeatureCustomizationDemo: React.FC = () => {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => { useEffect(() => {
if (hasAnimated) return;
const interval = setInterval(() => { const interval = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % colorPresets.length); setActiveIndex((prev) => {
}, 2500); const next = prev + 1;
if (next >= colorPresets.length) {
clearInterval(interval);
setHasAnimated(true);
return 0; // Reset to first
}
return next;
});
}, 1500);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [hasAnimated]);
const currentPreset = colorPresets[activeIndex]; const currentPreset = colorPresets[activeIndex];

View File

@ -61,19 +61,9 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
{t.features.unlimited.description} {t.features.unlimited.description}
</p> </p>
</div> </div>
<motion.div <div className="self-end">
animate={{
rotate: 360,
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'linear'
}}
className="self-end"
>
<InfinityIcon className="w-16 h-16 text-indigo-200" /> <InfinityIcon className="w-16 h-16 text-indigo-200" />
</motion.div> </div>
</div> </div>
</BentoItem> </BentoItem>
@ -89,21 +79,11 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
99.9% uptime guarantee with enterprise-grade infrastructure 99.9% uptime guarantee with enterprise-grade infrastructure
</p> </p>
</div> </div>
<motion.div <div className="self-end">
animate={{
scale: [1, 1.1, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut'
}}
className="self-end"
>
<div className="text-4xl font-bold text-green-600"> <div className="text-4xl font-bold text-green-600">
99.9<span className="text-2xl">%</span> 99.9<span className="text-2xl">%</span>
</div> </div>
</motion.div> </div>
</div> </div>
</BentoItem> </BentoItem>
@ -119,19 +99,9 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
Bank-level encryption and GDPR compliant data handling Bank-level encryption and GDPR compliant data handling
</p> </p>
</div> </div>
<motion.div <div className="self-end">
animate={{
rotateY: [0, 180, 360],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: 'easeInOut'
}}
className="self-end"
>
<Shield className="w-16 h-16 text-amber-200" /> <Shield className="w-16 h-16 text-amber-200" />
</motion.div> </div>
</div> </div>
</BentoItem> </BentoItem>
</BentoGrid> </BentoGrid>

View File

@ -7,9 +7,9 @@ import { Badge } from '@/components/ui/Badge';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { CheckCircle2, ArrowRight, Zap } from 'lucide-react'; import { CheckCircle2, ArrowRight, Zap } from 'lucide-react';
import { AnimatedBackground } from './AnimatedBackground'; import { AnimatedBackground } from './AnimatedBackground';
import { HeroQRInteractive } from './HeroQRInteractive'; import { QRTypesShowcase } from './QRTypesShowcase';
import { PhoneMockup } from './PhoneMockup';
import { StatsCounter, defaultStats } from './StatsCounter'; import { StatsCounter, defaultStats } from './StatsCounter';
import { slideUp, slideRight, staggerContainer, staggerItem } from '@/lib/animations';
interface HeroProps { interface HeroProps {
t: any; // i18n translation function 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"> <div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* Left: Content */} {/* Left: Content */}
<motion.div <motion.div
variants={staggerContainer} initial={{ opacity: 0, y: 20 }}
initial="initial" animate={{ opacity: 1, y: 0 }}
animate="animate" transition={{ duration: 0.5 }}
className="space-y-8 text-center lg:text-left" className="space-y-8 text-center lg:text-left"
> >
{/* Badge */} {/* 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"> <Badge variant="info" className="inline-flex items-center space-x-2 px-4 py-2 shadow-sm">
<Zap className="w-4 h-4" /> <Zap className="w-4 h-4" />
<span className="font-semibold">{t.hero.badge}</span> <span className="font-semibold">{t.hero.badge}</span>
@ -40,7 +45,12 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
</motion.div> </motion.div>
{/* Headline */} {/* 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"> <h2 className="text-5xl sm:text-6xl lg:text-7xl font-bold text-gray-900 leading-tight">
{t.hero.title} {t.hero.title}
</h2> </h2>
@ -51,13 +61,18 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
</motion.div> </motion.div>
{/* Feature List */} {/* 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) => ( {t.hero.features.map((feature: string, index: number) => (
<motion.div <motion.div
key={index} key={index}
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} 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" 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"> <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 */} {/* CTAs */}
<motion.div <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" className="flex flex-col sm:flex-row gap-4 pt-6 justify-center lg:justify-start"
> >
<Link href="/signup"> <Link href="/signup">
@ -97,7 +114,7 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} 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" className="flex items-center gap-2 text-sm text-gray-500 justify-center lg:justify-start"
> >
<div className="flex -space-x-2"> <div className="flex -space-x-2">
@ -112,19 +129,27 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
</motion.div> </motion.div>
</motion.div> </motion.div>
{/* Right: Interactive QR Generator */} {/* Right: QR Types Showcase + Phone */}
<motion.div <motion.div
initial={{ opacity: 0, x: 50 }} initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.6 }} transition={{ delay: 0.3, duration: 0.5 }}
className="relative" 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> </motion.div>
</div> </div>
{/* Stats Counter */} {/* Stats Counter - Subtiler */}
<StatsCounter stats={defaultStats} /> <div className="mt-20">
<StatsCounter stats={defaultStats} />
</div>
</div> </div>
{/* Smooth Gradient Fade Transition to next section */} {/* Smooth Gradient Fade Transition to next section */}

View File

@ -4,24 +4,37 @@ import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { Wifi, Battery, Signal } from 'lucide-react'; import { Wifi, Battery, Signal } from 'lucide-react';
import { qrTypes } from './QRTypesShowcase';
interface PhoneMockupProps { interface PhoneMockupProps {
url: string; url?: string;
foreground: string; foreground?: string;
background: string; background?: string;
autoRotate?: boolean;
} }
export const PhoneMockup: React.FC<PhoneMockupProps> = ({ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
url, url,
foreground, foreground = '#000000',
background, background = '#FFFFFF',
autoRotate = false,
}) => { }) => {
const [currentTypeIndex, setCurrentTypeIndex] = useState(0);
const [isScanning, setIsScanning] = useState(false); const [isScanning, setIsScanning] = useState(false);
const [showResult, setShowResult] = 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(() => { useEffect(() => {
if (!autoRotate) return;
const interval = setInterval(() => { const interval = setInterval(() => {
setCurrentTypeIndex((prev) => (prev + 1) % qrTypes.length);
// Trigger scan animation
setIsScanning(true); setIsScanning(true);
setTimeout(() => { setTimeout(() => {
setIsScanning(false); setIsScanning(false);
@ -29,30 +42,32 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
setTimeout(() => { setTimeout(() => {
setShowResult(false); setShowResult(false);
}, 2000); }, 2000);
}, 1500); }, 1000);
}, 6000); }, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [autoRotate]);
// Extract domain from URL for preview
const getDomain = (urlString: string) => { const getDomain = (urlString: string) => {
try { try {
const urlObj = new URL(urlString); if (urlString.startsWith('http')) {
return urlObj.hostname.replace('www.', ''); const urlObj = new URL(urlString);
return urlObj.hostname.replace('www.', '');
}
return urlString.substring(0, 20);
} catch { } catch {
return 'qrmaster.net'; return 'qrmaster.net';
} }
}; };
return ( return (
<div className="relative w-full max-w-[300px] mx-auto"> <div className="relative w-full max-w-[280px] mx-auto">
{/* Phone Frame */} {/* Phone Frame */}
<div className="relative"> <div className="relative">
{/* Phone outline */} {/* 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 */} {/* 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 */} {/* 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="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"> <div className="flex justify-between items-center">
@ -66,57 +81,59 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
</div> </div>
{/* Camera Notch */} {/* 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 */} {/* Screen Content */}
<div className="relative h-full bg-gray-50 pt-8"> <div className="relative h-full bg-gray-50 pt-8">
<div className="px-4 h-full flex flex-col"> <div className="px-4 h-full flex flex-col">
{/* Camera App Header */} {/* Camera App Header */}
<div className="text-center mb-4"> <div className="text-center mb-3">
<h3 className="font-semibold text-gray-900">QR Scanner</h3> <h3 className="font-semibold text-gray-900 text-sm">QR Scanner</h3>
</div> </div>
{/* QR Code Display Area */} {/* QR Code Display Area */}
<div className="flex-1 flex items-center justify-center relative"> <div className="flex-1 flex items-center justify-center relative">
{/* QR Code */} <AnimatePresence mode="wait">
<motion.div <motion.div
animate={{ key={autoRotate ? currentTypeIndex : displayUrl}
scale: isScanning ? 0.95 : 1, initial={{ scale: 0.8, opacity: 0 }}
}} animate={{ scale: isScanning ? 0.95 : 1, opacity: 1 }}
transition={{ duration: 0.3 }} exit={{ scale: 0.8, opacity: 0 }}
className="relative" transition={{ duration: 0.3 }}
> className="relative"
<div className="p-4 bg-white rounded-2xl shadow-lg"> >
<QRCodeSVG <div className="p-3 bg-white rounded-xl shadow-lg">
value={url || 'https://qrmaster.net'} <QRCodeSVG
size={140} value={displayUrl}
fgColor={foreground} size={120}
bgColor={background} fgColor={foreground}
level="M" bgColor={background}
/> level="M"
</div> />
</div>
{/* Scan Overlay */} {/* Scan Overlay */}
<AnimatePresence> <AnimatePresence>
{isScanning && ( {isScanning && (
<motion.div <motion.div
initial={{ y: -100, opacity: 0 }} initial={{ y: -80, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }} exit={{ y: 80, opacity: 0 }}
transition={{ duration: 1.5, ease: 'linear' }} transition={{ duration: 0.8, ease: 'linear' }}
className="absolute inset-0 flex items-center justify-center" 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" /> <div className="w-full h-0.5 bg-gradient-to-r from-transparent via-primary-500 to-transparent shadow-lg" />
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{/* Corner Brackets */} {/* 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-1 left-1 w-5 h-5 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 top-1 right-1 w-5 h-5 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-1 left-1 w-5 h-5 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" /> <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> </motion.div>
</AnimatePresence>
</div> </div>
{/* Result Notification */} {/* Result Notification */}
@ -126,17 +143,17 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
initial={{ y: 100, opacity: 0 }} initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }} 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 items-center gap-2">
<div className="flex-shrink-0 w-10 h-10 bg-success-100 rounded-full flex items-center justify-center"> <div className="flex-shrink-0 w-8 h-8 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"> <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" /> <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> </svg>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-gray-900">QR Code Scanned</p> <p className="text-xs font-semibold text-gray-900">{displayName}</p>
<p className="text-xs text-gray-500 truncate">{getDomain(url)}</p> <p className="text-[10px] text-gray-500 truncate">{getDomain(displayUrl)}</p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -144,9 +161,9 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
</AnimatePresence> </AnimatePresence>
{/* Instruction Text */} {/* Instruction Text */}
<div className="text-center pb-6"> <div className="text-center pb-4">
<p className="text-xs text-gray-500"> <p className="text-[10px] text-gray-500">
{isScanning ? 'Scanning...' : 'Point camera at QR code'} {isScanning ? 'Scanning...' : autoRotate ? 'Auto-scanning demo' : 'Point camera at QR code'}
</p> </p>
</div> </div>
</div> </div>
@ -154,24 +171,14 @@ export const PhoneMockup: React.FC<PhoneMockupProps> = ({
</div> </div>
{/* Side buttons */} {/* Side buttons */}
<div className="absolute right-0 top-20 w-1 h-12 bg-gray-800 rounded-r" /> <div className="absolute right-0 top-16 w-0.5 h-10 bg-gray-800 rounded-r" />
<div className="absolute right-0 top-36 w-1 h-8 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-24 w-1 h-16 bg-gray-800 rounded-l" /> <div className="absolute left-0 top-20 w-0.5 h-12 bg-gray-800 rounded-l" />
</div> </div>
{/* Phone Shadow */} {/* 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> </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> </div>
); );
}; };

View File

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

View File

@ -25,8 +25,8 @@ const AnimatedNumber: React.FC<{
const isInView = useInView(ref, { once: true, margin: '-100px' }); const isInView = useInView(ref, { once: true, margin: '-100px' });
const motionValue = useMotionValue(0); const motionValue = useMotionValue(0);
const springValue = useSpring(motionValue, { const springValue = useSpring(motionValue, {
damping: 60, damping: 80,
stiffness: 100, stiffness: 60,
}); });
const [displayValue, setDisplayValue] = React.useState('0'); const [displayValue, setDisplayValue] = React.useState('0');
@ -45,7 +45,7 @@ const AnimatedNumber: React.FC<{
}, [springValue, decimals]); }, [springValue, decimals]);
return ( 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} {prefix}
{displayValue} {displayValue}
{suffix} {suffix}
@ -57,16 +57,18 @@ export const StatsCounter: React.FC<StatsCounterProps> = ({ stats }) => {
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.5 }} viewport={{ once: true, margin: '-100px' }}
className="grid grid-cols-2 lg:grid-cols-4 gap-8 mt-16" transition={{ duration: 0.5 }}
className="grid grid-cols-2 lg:grid-cols-4 gap-6 mt-16"
> >
{stats.map((stat, index) => ( {stats.map((stat, index) => (
<motion.div <motion.div
key={index} key={index}
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.9 + index * 0.1, duration: 0.4 }} viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.3 }}
className="text-center" className="text-center"
> >
<div className="mb-2"> <div className="mb-2">
@ -77,7 +79,7 @@ export const StatsCounter: React.FC<StatsCounterProps> = ({ stats }) => {
decimals={stat.decimals} decimals={stat.decimals}
/> />
</div> </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>
))} ))}
</motion.div> </motion.div>