Compare commits

...

2 Commits

Author SHA1 Message Date
Timo 3c6d75b6bb 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>
2026-01-19 09:14:52 +01:00
Timo a7180e3b9b feat: Complete landing page modernization with premium design
Major overhaul of the landing page with modern, premium design while maintaining 100% SEO compatibility.

**Design System:**
- New animation library with premium easing curves
- Advanced shadow system with colored shadows
- Glassmorphism utilities
- Premium gradient palette
- Grid-based animated backgrounds

**Hero Section:**
- Interactive QR generator with real-time updates
- Color preset system with smooth morphing
- Animated background with modern grid pattern
- Stats counter with animated numbers
- Enhanced CTAs with hover effects

**Instant Generator:**
- Tab-based UI (Basic, Presets, Advanced)
- Visual preset gallery with 12 professional styles
- Phone mockup with scan animation
- Progressive disclosure UX
- Enhanced download experience

**Features Section:**
- Modern Bento Grid layout
- Animated analytics chart demo
- QR customization morphing demo
- Bulk creation animation
- Interactive feature cards

**Pricing:**
- Enhanced visual design
- Better shadows and gradients
- Improved hover states

**Technical:**
- All new components use Framer Motion
- Optimized animations with GPU acceleration
- Responsive design maintained
- SEO unchanged (server components intact)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 08:52:33 +01:00
18 changed files with 2125 additions and 364 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View File

@ -0,0 +1,77 @@
'use client';
import React from 'react';
/**
* Modern animated grid background for Hero section
* Replaces the overused "blob" animations with a more premium, tech-forward aesthetic
*/
export const AnimatedBackground: React.FC = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{/* Subtle gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 via-white to-purple-50" />
{/* Animated grid pattern */}
<svg
className="absolute inset-0 w-full h-full"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Gradient for grid lines */}
<linearGradient id="grid-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#6366f1" stopOpacity="0.1" />
<stop offset="50%" stopColor="#8b5cf6" stopOpacity="0.15" />
<stop offset="100%" stopColor="#d946ef" stopOpacity="0.1" />
</linearGradient>
{/* Pattern definition */}
<pattern
id="grid-pattern"
width="50"
height="50"
patternUnits="userSpaceOnUse"
>
{/* Vertical line */}
<line
x1="0"
y1="0"
x2="0"
y2="50"
stroke="url(#grid-gradient)"
strokeWidth="1"
strokeDasharray="4 4"
className="animate-dash"
/>
{/* Horizontal line */}
<line
x1="0"
y1="0"
x2="50"
y2="0"
stroke="url(#grid-gradient)"
strokeWidth="1"
strokeDasharray="4 4"
className="animate-dash"
/>
</pattern>
</defs>
{/* Grid overlay */}
<rect width="100%" height="100%" fill="url(#grid-pattern)" />
</svg>
{/* Floating gradient orbs (subtle, as accents) */}
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-blue-400/5 rounded-full blur-3xl animate-float" />
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-400/5 rounded-full blur-3xl animate-float" style={{ animationDelay: '2s' }} />
{/* Noise texture overlay for depth */}
<div
className="absolute inset-0 opacity-[0.015] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
}}
/>
</div>
);
};

View File

@ -0,0 +1,50 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
interface BentoGridProps {
children: React.ReactNode[];
}
/**
* Modern Bento Box Grid Layout
* Features are displayed in different sizes based on importance
*/
export const BentoGrid: React.FC<BentoGridProps> = ({ children }) => {
return (
<div className="grid grid-cols-1 md:grid-cols-6 gap-6 max-w-6xl mx-auto">
{children}
</div>
);
};
interface BentoItemProps {
children: React.ReactNode;
size?: 'small' | 'medium' | 'large';
className?: string;
}
export const BentoItem: React.FC<BentoItemProps> = ({
children,
size = 'medium',
className = '',
}) => {
const sizeClasses = {
small: 'md:col-span-2',
medium: 'md:col-span-3',
large: 'md:col-span-6',
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5 }}
className={`${sizeClasses[size]} ${className}`}
>
{children}
</motion.div>
);
};

View File

@ -0,0 +1,95 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { TrendingUp, Eye, MousePointer } from 'lucide-react';
export const FeatureAnalyticsDemo: React.FC = () => {
const data = [30, 45, 35, 60, 50, 75, 65, 85];
return (
<div className="relative h-full min-h-[300px] bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl p-8 overflow-hidden group">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<svg width="100%" height="100%">
<pattern id="analytics-grid" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="1" fill="currentColor" />
</pattern>
<rect width="100%" height="100%" fill="url(#analytics-grid)" />
</svg>
</div>
<div className="relative z-10 h-full flex flex-col">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-6 h-6 text-blue-600" />
<h3 className="text-2xl font-bold text-gray-900">Analytics</h3>
</div>
<p className="text-gray-600">Real-time tracking and insights</p>
</div>
{/* Animated Chart */}
<div className="flex-1 flex items-end gap-2 mb-6">
{data.map((value, index) => (
<motion.div
key={index}
initial={{ height: 0 }}
whileInView={{ height: `${value}%` }}
viewport={{ once: true }}
transition={{
duration: 0.8,
delay: index * 0.1,
ease: [0.34, 1.56, 0.64, 1]
}}
className="flex-1 bg-gradient-to-t from-blue-600 to-blue-400 rounded-t-lg relative group-hover:from-blue-500 group-hover:to-blue-300 transition-all"
>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 1 + index * 0.1 }}
className="absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-bold text-blue-700 whitespace-nowrap"
>
{value}
</motion.div>
</motion.div>
))}
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-3">
<div className="flex items-center gap-2 text-sm text-gray-600 mb-1">
<Eye className="w-4 h-4" />
<span>Total Scans</span>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 1.2 }}
className="text-2xl font-bold text-gray-900"
>
12,547
</motion.div>
</div>
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-3">
<div className="flex items-center gap-2 text-sm text-gray-600 mb-1">
<MousePointer className="w-4 h-4" />
<span>Click Rate</span>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 1.4 }}
className="text-2xl font-bold text-green-600"
>
87%
</motion.div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,111 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { QRCodeSVG } from 'qrcode.react';
import { FileSpreadsheet, ArrowRight } from 'lucide-react';
export const FeatureBulkDemo: React.FC = () => {
const [showQRs, setShowQRs] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => {
if (hasAnimated) return;
const timer = setTimeout(() => {
setShowQRs(true);
setHasAnimated(true);
}, 500);
return () => clearTimeout(timer);
}, [hasAnimated]);
const qrCodes = [
{ id: 1, url: 'product-1', delay: 0 },
{ id: 2, url: 'product-2', delay: 0.1 },
{ id: 3, url: 'product-3', delay: 0.2 },
{ id: 4, url: 'product-4', delay: 0.3 },
{ id: 5, url: 'product-5', delay: 0.4 },
{ id: 6, url: 'product-6', delay: 0.5 },
];
return (
<div className="relative h-full min-h-[300px] bg-gradient-to-br from-emerald-50 to-teal-100 rounded-2xl p-8 overflow-hidden">
<div className="relative z-10 h-full flex flex-col">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<FileSpreadsheet className="w-6 h-6 text-emerald-600" />
<h3 className="text-2xl font-bold text-gray-900">Bulk Creation</h3>
</div>
<p className="text-gray-600">Generate hundreds at once</p>
</div>
{/* Animation */}
<div className="flex-1 flex items-center justify-center gap-8">
{/* CSV Icon */}
<motion.div
animate={{
scale: showQRs ? 0.8 : 1,
opacity: showQRs ? 0.5 : 1,
}}
transition={{ duration: 0.5 }}
className="flex flex-col items-center"
>
<div className="w-20 h-24 bg-white rounded-lg shadow-md flex items-center justify-center border-2 border-emerald-200">
<FileSpreadsheet className="w-10 h-10 text-emerald-600" />
</div>
<p className="text-xs text-gray-600 mt-2 font-medium">products.csv</p>
</motion.div>
{/* Arrow */}
<motion.div
animate={{
x: showQRs ? [0, 10, 0] : 0,
opacity: showQRs ? 1 : 0.3,
}}
transition={{ duration: 0.5, repeat: showQRs ? Infinity : 0, repeatDelay: 1 }}
>
<ArrowRight className="w-8 h-8 text-emerald-600" />
</motion.div>
{/* QR Codes Grid */}
<div className="grid grid-cols-3 gap-2">
{qrCodes.map((qr) => (
<motion.div
key={qr.id}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: showQRs ? 1 : 0,
opacity: showQRs ? 1 : 0,
}}
transition={{
duration: 0.4,
delay: showQRs ? qr.delay : 0,
ease: [0.34, 1.56, 0.64, 1]
}}
className="p-1.5 bg-white rounded shadow-sm"
>
<QRCodeSVG
value={`https://qrmaster.net/${qr.url}`}
size={32}
level="L"
/>
</motion.div>
))}
</div>
</div>
{/* Counter */}
<motion.div
animate={{ opacity: showQRs ? 1 : 0 }}
className="text-center"
>
<p className="text-sm font-semibold text-emerald-700">
6 QR codes generated
</p>
</motion.div>
</div>
</div>
);
};

View File

@ -0,0 +1,93 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { QRCodeSVG } from 'qrcode.react';
import { Palette } from 'lucide-react';
const colorPresets = [
{ fg: '#000000', bg: '#FFFFFF' },
{ fg: '#0ea5e9', bg: '#e0f2fe' },
{ fg: '#8b5cf6', bg: '#f3e8ff' },
{ fg: '#f59e0b', bg: '#fef3c7' },
];
export const FeatureCustomizationDemo: React.FC = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => {
if (hasAnimated) return;
const interval = setInterval(() => {
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];
return (
<div className="relative h-full min-h-[300px] bg-gradient-to-br from-purple-50 to-pink-100 rounded-2xl p-8 overflow-hidden">
<div className="relative z-10 h-full flex flex-col">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-6 h-6 text-purple-600" />
<h3 className="text-2xl font-bold text-gray-900">Customization</h3>
</div>
<p className="text-gray-600">Brand your QR codes</p>
</div>
{/* QR Code with Morphing */}
<div className="flex-1 flex items-center justify-center">
<AnimatePresence mode="wait">
<motion.div
key={activeIndex}
initial={{ scale: 0.8, rotate: -10, opacity: 0 }}
animate={{ scale: 1, rotate: 0, opacity: 1 }}
exit={{ scale: 0.8, rotate: 10, opacity: 0 }}
transition={{ duration: 0.5, ease: [0.34, 1.56, 0.64, 1] }}
className="p-6 bg-white rounded-2xl shadow-lg"
>
<QRCodeSVG
value="https://qrmaster.net"
size={140}
fgColor={currentPreset.fg}
bgColor={currentPreset.bg}
level="M"
/>
</motion.div>
</AnimatePresence>
</div>
{/* Color Dots Indicator */}
<div className="flex justify-center gap-2 mt-6">
{colorPresets.map((preset, index) => (
<motion.button
key={index}
onClick={() => setActiveIndex(index)}
className={`w-3 h-3 rounded-full transition-all ${
index === activeIndex ? 'scale-125' : 'scale-100 opacity-50'
}`}
style={{
background: `linear-gradient(135deg, ${preset.fg}, ${preset.bg})`
}}
whileHover={{ scale: 1.3 }}
whileTap={{ scale: 0.9 }}
/>
))}
</div>
</div>
</div>
);
};

View File

@ -2,83 +2,109 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { BentoGrid, BentoItem } from './BentoGrid';
import { FeatureAnalyticsDemo } from './FeatureAnalyticsDemo';
import { FeatureCustomizationDemo } from './FeatureCustomizationDemo';
import { FeatureBulkDemo } from './FeatureBulkDemo';
import { Infinity as InfinityIcon, Clock, Shield } from 'lucide-react';
interface FeaturesProps {
t: any; // i18n translation function
}
export const Features: React.FC<FeaturesProps> = ({ t }) => {
const features = [
{
key: 'analytics',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
color: 'text-blue-600 bg-blue-100',
},
{
key: 'customization',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
),
color: 'text-purple-600 bg-purple-100',
},
{
key: 'unlimited',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
color: 'text-green-600 bg-green-100',
},
];
return (
<section className="py-16 bg-gray-50">
<section className="py-20 bg-gray-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
<h2 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
{t.features.title}
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Everything you need to create, manage, and track QR codes at scale
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{features.map((feature, index) => (
<motion.div
key={feature.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card hover className="h-full border-gray-100 hover:border-primary-100 hover:shadow-lg transition-all">
<CardHeader>
<div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}>
{feature.icon}
</div>
<CardTitle>{t.features[feature.key].title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
{t.features[feature.key].description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
{/* Bento Grid */}
<BentoGrid>
{/* Analytics - Large */}
<BentoItem size="large">
<FeatureAnalyticsDemo />
</BentoItem>
{/* Customization - Medium */}
<BentoItem size="medium">
<FeatureCustomizationDemo />
</BentoItem>
{/* Bulk Creation - Medium */}
<BentoItem size="medium">
<FeatureBulkDemo />
</BentoItem>
{/* Unlimited - Small */}
<BentoItem size="small">
<div className="h-full min-h-[200px] bg-gradient-to-br from-indigo-50 to-indigo-100 rounded-2xl p-6 flex flex-col justify-between">
<div>
<InfinityIcon className="w-8 h-8 text-indigo-600 mb-3" />
<h3 className="text-xl font-bold text-gray-900 mb-2">
{t.features.unlimited.title}
</h3>
<p className="text-sm text-gray-600">
{t.features.unlimited.description}
</p>
</div>
<div className="self-end">
<InfinityIcon className="w-16 h-16 text-indigo-200" />
</div>
</div>
</BentoItem>
{/* Reliable - Small */}
<BentoItem size="small">
<div className="h-full min-h-[200px] bg-gradient-to-br from-green-50 to-green-100 rounded-2xl p-6 flex flex-col justify-between">
<div>
<Clock className="w-8 h-8 text-green-600 mb-3" />
<h3 className="text-xl font-bold text-gray-900 mb-2">
24/7 Reliable
</h3>
<p className="text-sm text-gray-600">
99.9% uptime guarantee with enterprise-grade infrastructure
</p>
</div>
<div className="self-end">
<div className="text-4xl font-bold text-green-600">
99.9<span className="text-2xl">%</span>
</div>
</div>
</div>
</BentoItem>
{/* Secure - Small */}
<BentoItem size="small">
<div className="h-full min-h-[200px] bg-gradient-to-br from-amber-50 to-amber-100 rounded-2xl p-6 flex flex-col justify-between">
<div>
<Shield className="w-8 h-8 text-amber-600 mb-3" />
<h3 className="text-xl font-bold text-gray-900 mb-2">
Enterprise Security
</h3>
<p className="text-sm text-gray-600">
Bank-level encryption and GDPR compliant data handling
</p>
</div>
<div className="self-end">
<Shield className="w-16 h-16 text-amber-200" />
</div>
</div>
</BentoItem>
</BentoGrid>
</div>
</section>
);

View File

@ -4,152 +4,156 @@ import React from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card } from '@/components/ui/Card';
import { motion } from 'framer-motion';
import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight } from 'lucide-react';
import { CheckCircle2, ArrowRight, Zap } from 'lucide-react';
import { AnimatedBackground } from './AnimatedBackground';
import { QRTypesShowcase } from './QRTypesShowcase';
import { PhoneMockup } from './PhoneMockup';
import { StatsCounter, defaultStats } from './StatsCounter';
interface HeroProps {
t: any; // i18n translation function
}
export const Hero: React.FC<HeroProps> = ({ t }) => {
const templateCards = [
{ title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
{ title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
{ title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
{ title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
];
const containerjs = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemjs = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
{/* Animated Background Orbs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{/* Orb 1 - Blue (top-left) */}
<div className="absolute -top-24 -left-24 w-96 h-96 bg-blue-400/30 rounded-full blur-3xl animate-blob" />
{/* Orb 2 - Purple (top-right) */}
<div className="absolute -top-12 -right-12 w-96 h-96 bg-purple-400/30 rounded-full blur-3xl animate-blob animation-delay-2000" />
{/* Orb 3 - Pink (bottom-left) */}
<div className="absolute -bottom-24 -left-12 w-96 h-96 bg-pink-400/20 rounded-full blur-3xl animate-blob animation-delay-4000" />
{/* Orb 4 - Cyan (center-right) */}
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-cyan-400/20 rounded-full blur-3xl animate-blob animation-delay-6000" />
</div>
<section className="relative overflow-hidden pt-20 pb-32 min-h-[90vh] flex items-center">
{/* Modern Animated Background */}
<AnimatedBackground />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Content */}
<div className="space-y-8">
<Badge variant="info" className="inline-flex items-center space-x-2">
<span>{t.hero.badge}</span>
</Badge>
{/* Main Content Grid */}
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* Left: Content */}
<motion.div
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
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>
</Badge>
</motion.div>
{/* Headline */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
transition={{ delay: 0.2, duration: 0.4 }}
className="space-y-6"
>
<h2 className="text-5xl lg:text-6xl 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}
</h2>
<p className="text-xl text-gray-600 leading-relaxed max-w-2xl">
<p className="text-xl lg:text-2xl text-gray-600 leading-relaxed max-w-2xl mx-auto lg:mx-0">
{t.hero.subtitle}
</p>
<div className="space-y-3 pt-2">
{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.2 + (index * 0.1) }}
className="flex items-center space-x-3"
>
<div className="flex-shrink-0 w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center">
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
</div>
<span className="text-gray-700 font-medium">{feature}</span>
</motion.div>
))}
</div>
</motion.div>
{/* Feature List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4 pt-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="space-y-4 pt-4"
>
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-6 w-full sm:w-auto shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40 transition-all duration-300">
{t.hero.cta_primary}
</Button>
</Link>
<Link href="/#pricing">
<Button variant="outline" size="lg" className="text-lg px-8 py-6 w-full sm:w-auto backdrop-blur-sm bg-white/50 border-gray-200 hover:bg-white/80 transition-all duration-300">
{t.hero.cta_secondary}
</Button>
</Link>
</motion.div>
</div>
{/* Right Preview Widget */}
<div className="relative">
<motion.div
variants={containerjs}
initial="hidden"
animate="show"
className="grid grid-cols-2 gap-4"
>
{templateCards.map((card, index) => (
<motion.div key={index} variants={itemjs}>
<Card className={`backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-6 text-center hover:scale-105 transition-all duration-300 group cursor-pointer`}>
<div className={`w-12 h-12 mx-auto mb-4 rounded-xl ${card.color} flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<card.icon className="w-6 h-6" />
</div>
<p className="font-semibold text-gray-800 group-hover:text-gray-900">{card.title}</p>
</Card>
{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.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">
<CheckCircle2 className="w-4 h-4 text-white" />
</div>
<span className="text-gray-700 font-medium text-lg">{feature}</span>
</motion.div>
))}
</motion.div>
{/* Floating Badge */}
{/* CTAs */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 }}
className="absolute -top-4 -right-4 bg-gradient-to-r from-success-500 to-emerald-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg shadow-success-500/30 flex items-center gap-2"
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"
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
{t.hero.engagement_badge}
<Link href="/signup">
<Button
size="lg"
className="text-lg px-10 py-7 w-full sm:w-auto shadow-primary-lg hover:shadow-primary-lg hover:scale-105 transition-all duration-300 group"
>
<span>{t.hero.cta_primary}</span>
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
<Link href="/#pricing">
<Button
variant="outline"
size="lg"
className="text-lg px-10 py-7 w-full sm:w-auto glass border-gray-300 hover:glass-strong hover:scale-105 transition-all duration-300"
>
{t.hero.cta_secondary}
</Button>
</Link>
</motion.div>
</div>
{/* Trust Indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
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">
{[1, 2, 3].map((i) => (
<div
key={i}
className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-400 border-2 border-white"
/>
))}
</div>
<span>Trusted by 25,000+ professionals worldwide</span>
</motion.div>
</motion.div>
{/* Right: QR Types Showcase + Phone */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.5 }}
className="relative space-y-8"
>
{/* QR Types Grid */}
<QRTypesShowcase />
{/* Phone Mockup with Auto-Rotation */}
<div className="flex justify-center">
<PhoneMockup autoRotate={true} />
</div>
</motion.div>
</div>
{/* Stats Counter - Subtiler */}
<div className="mt-20">
<StatsCounter stats={defaultStats} />
</div>
</div>
{/* Smooth Gradient Fade Transition */}
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
</section >
{/* Smooth Gradient Fade Transition to next section */}
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-white pointer-events-none" />
</section>
);
};

View File

@ -0,0 +1,201 @@
'use client';
import React, { useState, useCallback } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { motion, AnimatePresence } from 'framer-motion';
import { Input } from '@/components/ui/Input';
import { Sparkles } from 'lucide-react';
interface ColorPreset {
name: string;
fg: string;
bg: string;
gradient?: string;
}
const colorPresets: ColorPreset[] = [
{ name: 'Classic', fg: '#000000', bg: '#FFFFFF' },
{ name: 'Ocean', fg: '#0ea5e9', bg: '#e0f2fe' },
{ name: 'Forest', fg: '#059669', bg: '#d1fae5' },
{ name: 'Sunset', fg: '#f59e0b', bg: '#fef3c7' },
{ name: 'Purple', fg: '#8b5cf6', bg: '#f3e8ff' },
{ name: 'Pink', fg: '#ec4899', bg: '#fce7f3' },
];
export const HeroQRInteractive: React.FC = () => {
const [url, setUrl] = useState('https://qrmaster.net');
const [foreground, setForeground] = useState('#000000');
const [background, setBackground] = useState('#FFFFFF');
const [selectedPreset, setSelectedPreset] = useState(0);
const handlePresetClick = useCallback((preset: ColorPreset, index: number) => {
setForeground(preset.fg);
setBackground(preset.bg);
setSelectedPreset(index);
}, []);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrl(e.target.value || 'https://qrmaster.net');
};
return (
<div className="relative">
{/* Main QR Container */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, ease: [0.34, 1.56, 0.64, 1] }}
className="relative"
>
{/* Glass card container */}
<div className="glass-strong rounded-3xl p-8 lg:p-12 shadow-elevation-4 relative overflow-hidden">
{/* Decorative corner accents */}
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-primary-vibrant opacity-10 blur-3xl" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-gradient-accent opacity-10 blur-3xl" />
{/* QR Code Display */}
<div className="flex justify-center mb-8">
<motion.div
key={`${foreground}-${background}`}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.3 }}
className="relative group"
>
{/* QR Code with hover effect */}
<div className="relative p-6 bg-white rounded-2xl shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<AnimatePresence mode="wait">
<motion.div
key={url}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<QRCodeSVG
value={url}
size={280}
fgColor={foreground}
bgColor={background}
level="H"
includeMargin={false}
/>
</motion.div>
</AnimatePresence>
{/* Scan indicator */}
<motion.div
className="absolute inset-0 border-2 border-primary-500/30 rounded-2xl"
initial={{ scale: 1, opacity: 0 }}
animate={{
scale: [1, 1.05, 1],
opacity: [0, 0.5, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
{/* Live indicator */}
<motion.div
className="absolute -top-3 -right-3 bg-gradient-to-r from-success-500 to-emerald-500 text-white px-3 py-1.5 rounded-full text-xs font-semibold shadow-lg flex items-center gap-2"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.5, type: 'spring' }}
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
Live Preview
</motion.div>
</motion.div>
</div>
{/* Input Field */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="mb-6"
>
<div className="relative">
<Input
value={url}
onChange={handleUrlChange}
placeholder="Enter your URL..."
className="text-center text-lg font-medium pr-12 bg-white/80 backdrop-blur-sm border-2 border-gray-200 focus:border-primary-400 transition-colors"
/>
<Sparkles className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-primary-500" />
</div>
</motion.div>
{/* Color Presets */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.4 }}
>
<p className="text-center text-sm font-medium text-gray-600 mb-3">
Choose a style:
</p>
<div className="flex justify-center gap-2 flex-wrap">
{colorPresets.map((preset, index) => (
<motion.button
key={preset.name}
onClick={() => handlePresetClick(preset, index)}
className={`relative px-4 py-2 rounded-xl font-medium text-sm transition-all ${
selectedPreset === index
? 'bg-white shadow-md ring-2 ring-primary-500 ring-offset-2'
: 'bg-white/60 hover:bg-white hover:shadow-sm'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 rounded-full border-2 border-gray-200"
style={{
background: `linear-gradient(135deg, ${preset.fg} 0%, ${preset.bg} 100%)`,
}}
/>
<span style={{ color: preset.fg }}>{preset.name}</span>
</div>
</motion.button>
))}
</div>
</motion.div>
</div>
</motion.div>
{/* Decorative floating elements */}
<motion.div
className="absolute -top-8 -left-8 w-16 h-16 bg-blue-400/20 rounded-full blur-xl"
animate={{
y: [-10, 10, -10],
scale: [1, 1.1, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<motion.div
className="absolute -bottom-8 -right-8 w-20 h-20 bg-purple-400/20 rounded-full blur-xl"
animate={{
y: [10, -10, 10],
scale: [1.1, 1, 1.1],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
);
};

View File

@ -2,30 +2,48 @@
import React, { useState } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils';
import AdBanner from '@/components/ads/AdBanner';
import { PresetGallery, qrPresets, type QRPreset } from './PresetGallery';
import { PhoneMockup } from './PhoneMockup';
import { Download, Smartphone, Palette, Settings2, Sparkles } from 'lucide-react';
interface InstantGeneratorProps {
t: any; // i18n translation function
}
type TabType = 'basic' | 'presets' | 'advanced';
export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
const [url, setUrl] = useState('https://example.com');
const [activeTab, setActiveTab] = useState<TabType>('basic');
const [url, setUrl] = useState('https://qrmaster.net');
const [foregroundColor, setForegroundColor] = useState('#000000');
const [backgroundColor, setBackgroundColor] = useState('#FFFFFF');
const [selectedPreset, setSelectedPreset] = useState('bold-1');
const [cornerStyle, setCornerStyle] = useState('square');
const [size, setSize] = useState(200);
const [size, setSize] = useState(256);
const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5;
const tabs = [
{ id: 'basic' as TabType, label: 'Basic', icon: Sparkles },
{ id: 'presets' as TabType, label: 'Presets', icon: Palette },
{ id: 'advanced' as TabType, label: 'Advanced', icon: Settings2 },
];
const handlePresetSelect = (preset: QRPreset) => {
setForegroundColor(preset.fg);
setBackgroundColor(preset.bg);
setSelectedPreset(preset.id);
};
const downloadQR = (format: 'svg' | 'png') => {
const svg = document.querySelector('#instant-qr-preview svg');
const svg = document.querySelector('#generator-qr-preview svg');
if (!svg || !url) return;
if (format === 'svg') {
@ -40,13 +58,12 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
} else {
// Convert SVG to PNG using Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
const svgData = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const blobUrl = URL.createObjectURL(blob);
img.onload = () => {
canvas.width = size;
@ -56,9 +73,9 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
ctx.fillRect(0, 0, size, size);
ctx.drawImage(img, 0, 0, size, size);
}
canvas.toBlob((blob) => {
if (blob) {
const downloadUrl = URL.createObjectURL(blob);
canvas.toBlob((canvasBlob) => {
if (canvasBlob) {
const downloadUrl = URL.createObjectURL(canvasBlob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = 'qrcode.png';
@ -68,19 +85,16 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
URL.revokeObjectURL(downloadUrl);
}
});
URL.revokeObjectURL(url);
URL.revokeObjectURL(blobUrl);
};
img.src = url;
img.src = blobUrl;
}
};
return (
<section className="pt-16 pb-32 bg-gray-50 border-t border-gray-100 relative">
<div
className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-r from-blue-50 to-white pointer-events-none"
style={{ maskImage: 'linear-gradient(to bottom, transparent, black)', WebkitMaskImage: 'linear-gradient(to bottom, transparent, black)' }}
/>
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@ -88,193 +102,243 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
<Badge variant="info" className="mb-4">
<Sparkles className="w-4 h-4 mr-2" />
Try It Now
</Badge>
<h2 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
{t.generator.title}
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Create and customize your QR code in seconds. No signup required.
</p>
</motion.div>
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
{/* Left Form */}
<div className="grid lg:grid-cols-2 gap-8 lg:gap-12 max-w-6xl mx-auto">
{/* Left: Controls */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card className="space-y-6 shadow-xl shadow-gray-200/50 border-gray-100">
<Input
label="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder={t.generator.url_placeholder}
className="transition-all focus:ring-2 focus:ring-primary-500/20"
/>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="foreground-color" className="block text-sm font-medium text-gray-700 mb-2">
{t.generator.foreground}
</label>
<div className="flex items-center space-x-2">
<input
id="foreground-color"
type="color"
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="w-14 h-12 rounded border border-gray-300 cursor-pointer"
aria-label="Foreground color picker"
/>
<Input
id="foreground-color-text"
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1"
aria-label="Foreground color hex value"
/>
</div>
</div>
<div>
<label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2">
{t.generator.background}
</label>
<div className="flex items-center space-x-2">
<input
id="background-color"
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-14 h-12 rounded border border-gray-300 cursor-pointer"
aria-label="Background color picker"
/>
<Input
id="background-color-text"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1"
aria-label="Background color hex value"
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2">
{t.generator.corners}
</label>
<select
id="corner-style"
value={cornerStyle}
onChange={(e) => setCornerStyle(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
<Card className="shadow-elevation-3 border-gray-100">
{/* Tab Navigation */}
<div className="flex gap-2 mb-6 p-1 bg-gray-100 rounded-xl">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg font-medium text-sm transition-all ${
activeTab === tab.id
? 'bg-white text-primary-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<option value="square">Square</option>
<option value="rounded">Rounded</option>
</select>
</div>
<div>
<label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2">
{t.generator.size}
</label>
<input
id="qr-size"
type="range"
min="100"
max="400"
value={size}
onChange={(e) => setSize(Number(e.target.value))}
className="w-full accent-primary-600"
aria-label={`QR code size: ${size} pixels`}
/>
<div className="text-sm text-gray-500 text-center mt-1" aria-hidden="true">{size}px</div>
</div>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
<div className="flex items-center justify-between">
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
</Badge>
<div className="text-sm text-gray-500">
Contrast: {contrast.toFixed(1)}:1
</div>
</div>
{/* Tab Content */}
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="space-y-6"
>
{/* Basic Tab */}
{activeTab === 'basic' && (
<>
<Input
label="Your URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://your-website.com"
/>
<div className="flex space-x-3">
<Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('svg')}>
{t.generator.download_svg}
</Button>
<Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('png')}>
{t.generator.download_png}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foreground
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
/>
<Input
value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1 font-mono text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Background
</label>
<div className="flex items-center space-x-2">
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
/>
<Input
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1 font-mono text-sm"
/>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2">
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
{hasGoodContrast ? '✓ Good contrast' : '⚠ Low contrast'}
</Badge>
<span className="text-sm text-gray-500">
Ratio: {contrast.toFixed(1)}:1
</span>
</div>
</>
)}
{/* Presets Tab */}
{activeTab === 'presets' && (
<>
<Input
label="Your URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://your-website.com"
/>
<PresetGallery
selectedPreset={selectedPreset}
onPresetSelect={handlePresetSelect}
url={url}
/>
</>
)}
{/* Advanced Tab */}
{activeTab === 'advanced' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Corner Style
</label>
<select
value={cornerStyle}
onChange={(e) => setCornerStyle(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="square">Square</option>
<option value="rounded">Rounded</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Size: {size}px
</label>
<input
type="range"
min="128"
max="512"
step="32"
value={size}
onChange={(e) => setSize(Number(e.target.value))}
className="w-full accent-primary-600"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>128px</span>
<span>512px</span>
</div>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
<p className="text-sm text-blue-900">
<strong>Pro Tip:</strong> Higher resolution is better for print materials. For digital use, 256px is usually sufficient.
</p>
</div>
</>
)}
</motion.div>
</AnimatePresence>
{/* Download Actions */}
<div className="mt-8 pt-6 border-t border-gray-100 space-y-3">
<div className="grid grid-cols-2 gap-3">
<Button
variant="outline"
onClick={() => downloadQR('svg')}
className="hover:bg-gray-50"
>
<Download className="w-4 h-4 mr-2" />
SVG
</Button>
<Button
variant="outline"
onClick={() => downloadQR('png')}
className="hover:bg-gray-50"
>
<Download className="w-4 h-4 mr-2" />
PNG
</Button>
</div>
<Button
className="w-full shadow-primary-md hover:shadow-primary-lg transition-all"
onClick={() => window.location.href = '/signup'}
>
<Sparkles className="w-4 h-4 mr-2" />
Save & Track Analytics
</Button>
</div>
<Button className="w-full text-lg py-6 shadow-lg shadow-primary-500/20 hover:shadow-primary-500/40 transition-all" onClick={() => window.location.href = '/login'}>
{t.generator.save_track}
</Button>
</Card>
</motion.div>
{/* Right Preview */}
{/* Right: Preview */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex flex-col items-center justify-center p-8 bg-gradient-to-br from-blue-50/50 to-purple-50/50 rounded-2xl border border-blue-100/50 shadow-lg shadow-blue-500/5 relative overflow-hidden backdrop-blur-sm"
className="space-y-8"
>
{/* Artistic Curved Lines Background */}
<div className="absolute inset-0 opacity-[0.4]">
<svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#60a5fa" stopOpacity="0.4" />
<stop offset="100%" stopColor="#c084fc" stopOpacity="0.4" />
</linearGradient>
<linearGradient id="gradient2" x1="100%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#818cf8" stopOpacity="0.4" />
<stop offset="100%" stopColor="#38bdf8" stopOpacity="0.4" />
</linearGradient>
</defs>
<path d="M0 100 Q 25 30 50 70 T 100 0" fill="none" stroke="url(#gradient1)" strokeWidth="0.8" className="opacity-60" />
<path d="M0 50 Q 40 80 70 30 T 100 50" fill="none" stroke="url(#gradient2)" strokeWidth="0.8" className="opacity-60" />
<path d="M0 0 Q 30 60 60 20 T 100 80" fill="none" stroke="url(#gradient1)" strokeWidth="0.6" className="opacity-40" />
</svg>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-15 brightness-100 contrast-150 mix-blend-overlay"></div>
{/* Large QR Preview */}
<div className="flex justify-center">
<div id="generator-qr-preview" className="relative group">
<div
className={`p-8 bg-white rounded-3xl shadow-elevation-3 border-2 border-gray-100 transition-all duration-300 group-hover:shadow-elevation-4 group-hover:scale-105 ${
cornerStyle === 'rounded' ? 'overflow-hidden' : ''
}`}
>
<QRCodeSVG
value={url}
size={size}
fgColor={foregroundColor}
bgColor={backgroundColor}
level="H"
/>
</div>
</div>
</div>
{/* Decorative Orbs */}
<div className="absolute -top-20 -right-20 w-64 h-64 bg-purple-200/30 rounded-full blur-3xl animate-blob"></div>
<div className="absolute -bottom-20 -left-20 w-64 h-64 bg-blue-200/30 rounded-full blur-3xl animate-blob animation-delay-2000"></div>
<div className="text-center w-full relative z-10">
<h3 className="text-xl font-bold mb-8 text-gray-800">{t.generator.live_preview}</h3>
<div id="instant-qr-preview" className="flex justify-center mb-8 transform hover:scale-105 transition-transform duration-300">
{url ? (
<div className={`${cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''} p-4 bg-white shadow-lg rounded-xl`}>
<QRCodeSVG
value={url}
size={size}
fgColor={foregroundColor}
bgColor={backgroundColor}
level="M"
/>
</div>
) : (
<div
className="bg-gray-100 rounded-xl flex items-center justify-center text-gray-500 animate-pulse"
style={{ width: 200, height: 200 }}
>
Enter URL
</div>
)}
</div>
<div className="text-sm font-medium text-gray-600 mb-2 bg-gray-50 py-2 px-4 rounded-full inline-block">
{url || 'https://example.com'}
</div>
<div className="text-xs text-gray-400 mt-2">{t.generator.demo_note}</div>
</div>
{/* Phone Mockup Preview */}
<PhoneMockup
url={url}
foreground={foregroundColor}
background={backgroundColor}
/>
</motion.div>
</div>
</div>

View File

@ -0,0 +1,184 @@
'use client';
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;
autoRotate?: boolean;
}
export const PhoneMockup: React.FC<PhoneMockupProps> = ({
url,
foreground = '#000000',
background = '#FFFFFF',
autoRotate = false,
}) => {
const [currentTypeIndex, setCurrentTypeIndex] = useState(0);
const [isScanning, setIsScanning] = useState(false);
const [showResult, setShowResult] = useState(false);
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);
setShowResult(true);
setTimeout(() => {
setShowResult(false);
}, 2000);
}, 1000);
}, 5000);
return () => clearInterval(interval);
}, [autoRotate]);
const getDomain = (urlString: string) => {
try {
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-[280px] mx-auto">
{/* Phone Frame */}
<div className="relative">
{/* Phone outline */}
<div className="relative bg-gray-900 rounded-[2.5rem] p-2.5 shadow-2xl">
{/* Screen */}
<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">
<span className="text-xs font-semibold">9:41</span>
<div className="flex items-center gap-1">
<Signal className="w-3 h-3" />
<Wifi className="w-3 h-3" />
<Battery className="w-3 h-3" />
</div>
</div>
</div>
{/* Camera Notch */}
<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-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">
<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: -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-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 */}
<AnimatePresence>
{showResult && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="mb-3 p-3 bg-white rounded-xl shadow-lg border border-gray-100"
>
<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-xs font-semibold text-gray-900">{displayName}</p>
<p className="text-[10px] text-gray-500 truncate">{getDomain(displayUrl)}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Instruction Text */}
<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>
</div>
</div>
{/* Side buttons */}
<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-[2.5rem] -z-10 blur-xl translate-y-3" />
</div>
</div>
);
};

View File

@ -0,0 +1,135 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { QRCodeSVG } from 'qrcode.react';
export interface QRPreset {
id: string;
name: string;
category: 'bold' | 'minimal' | 'gradient' | 'brand';
fg: string;
bg: string;
description?: string;
}
export const qrPresets: QRPreset[] = [
// Bold
{ id: 'bold-1', name: 'Classic Black', category: 'bold', fg: '#000000', bg: '#FFFFFF', description: 'Timeless professional' },
{ id: 'bold-2', name: 'Deep Ocean', category: 'bold', fg: '#0369a1', bg: '#e0f2fe', description: 'Trust and reliability' },
{ id: 'bold-3', name: 'Forest Green', category: 'bold', fg: '#047857', bg: '#d1fae5', description: 'Growth and nature' },
{ id: 'bold-4', name: 'Royal Purple', category: 'bold', fg: '#7c3aed', bg: '#f3e8ff', description: 'Premium and luxury' },
// Minimal
{ id: 'minimal-1', name: 'Soft Gray', category: 'minimal', fg: '#6b7280', bg: '#f9fafb', description: 'Subtle elegance' },
{ id: 'minimal-2', name: 'Gentle Blue', category: 'minimal', fg: '#60a5fa', bg: '#eff6ff', description: 'Calm and clean' },
{ id: 'minimal-3', name: 'Muted Green', category: 'minimal', fg: '#34d399', bg: '#ecfdf5', description: 'Fresh and light' },
{ id: 'minimal-4', name: 'Soft Pink', category: 'minimal', fg: '#f472b6', bg: '#fdf2f8', description: 'Friendly warmth' },
// Gradient-inspired
{ id: 'gradient-1', name: 'Sunset', category: 'gradient', fg: '#f59e0b', bg: '#fef3c7', description: 'Warm energy' },
{ id: 'gradient-2', name: 'Cotton Candy', category: 'gradient', fg: '#ec4899', bg: '#fce7f3', description: 'Playful fun' },
{ id: 'gradient-3', name: 'Sky Blue', category: 'gradient', fg: '#0ea5e9', bg: '#e0f2fe', description: 'Open freedom' },
{ id: 'gradient-4', name: 'Mint Fresh', category: 'gradient', fg: '#14b8a6', bg: '#ccfbf1', description: 'Modern clean' },
];
interface PresetGalleryProps {
selectedPreset: string;
onPresetSelect: (preset: QRPreset) => void;
url: string;
}
export const PresetGallery: React.FC<PresetGalleryProps> = ({
selectedPreset,
onPresetSelect,
url,
}) => {
const [activeCategory, setActiveCategory] = React.useState<string>('all');
const categories = ['all', 'bold', 'minimal', 'gradient'] as const;
const filteredPresets = activeCategory === 'all'
? qrPresets
: qrPresets.filter(p => p.category === activeCategory);
return (
<div className="space-y-6">
{/* Category Filter */}
<div className="flex gap-2 flex-wrap">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all capitalize ${
activeCategory === cat
? 'bg-primary-600 text-white shadow-md'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{cat}
</button>
))}
</div>
{/* Preset Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{filteredPresets.map((preset, index) => (
<motion.button
key={preset.id}
onClick={() => onPresetSelect(preset)}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.05, duration: 0.3 }}
className={`group relative p-4 rounded-xl border-2 transition-all ${
selectedPreset === preset.id
? 'border-primary-500 bg-primary-50 shadow-md'
: 'border-gray-200 bg-white hover:border-primary-300 hover:shadow-sm'
}`}
>
{/* Mini QR Preview */}
<div className="flex justify-center mb-3">
<div
className="p-2 rounded-lg transition-transform group-hover:scale-105"
style={{ backgroundColor: preset.bg }}
>
<QRCodeSVG
value={url || 'https://qrmaster.net'}
size={64}
fgColor={preset.fg}
bgColor={preset.bg}
level="M"
/>
</div>
</div>
{/* Preset Info */}
<div className="text-center">
<p className="font-semibold text-sm text-gray-900 mb-1">
{preset.name}
</p>
<p className="text-xs text-gray-500 line-clamp-1">
{preset.description}
</p>
</div>
{/* Selected Indicator */}
{selectedPreset === preset.id && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute top-2 right-2 w-6 h-6 bg-primary-600 rounded-full flex items-center justify-center"
>
<svg className="w-4 h-4 text-white" 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>
</motion.div>
)}
{/* Hover Glow */}
<div className="absolute inset-0 rounded-xl bg-gradient-primary opacity-0 group-hover:opacity-5 transition-opacity pointer-events-none" />
</motion.button>
))}
</div>
</div>
);
};

View File

@ -31,19 +31,19 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
];
return (
<section id="pricing" className="py-16">
<section id="pricing" className="py-20 bg-gradient-to-b from-gray-50 to-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
<h2 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
{t.pricing.subtitle}
</p>
</motion.div>
@ -63,11 +63,15 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
className="h-full"
>
<Card
className={`h-full flex flex-col ${plan.popular
? 'border-primary-500 shadow-xl relative scale-105 z-10'
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
className={`h-full flex flex-col relative overflow-hidden ${plan.popular
? 'border-primary-500 shadow-primary-lg scale-105 z-10 bg-gradient-to-br from-white to-blue-50'
: 'border-gray-200 hover:border-primary-200 hover:shadow-elevation-3 transition-all'
}`}
>
{/* Gradient overlay for popular */}
{plan.popular && (
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-primary-vibrant opacity-5 blur-3xl" />
)}
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
<Badge variant="info" className="px-4 py-1.5 shadow-sm">

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

@ -0,0 +1,116 @@
'use client';
import React from 'react';
import { motion, useInView, useMotionValue, useSpring } from 'framer-motion';
interface Stat {
label: string;
value: number;
suffix?: string;
prefix?: string;
decimals?: number;
}
interface StatsCounterProps {
stats: Stat[];
}
const AnimatedNumber: React.FC<{
value: number;
prefix?: string;
suffix?: string;
decimals?: number;
}> = ({ value, prefix = '', suffix = '', decimals = 0 }) => {
const ref = React.useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const motionValue = useMotionValue(0);
const springValue = useSpring(motionValue, {
damping: 80,
stiffness: 60,
});
const [displayValue, setDisplayValue] = React.useState('0');
React.useEffect(() => {
if (isInView) {
motionValue.set(value);
}
}, [isInView, motionValue, value]);
React.useEffect(() => {
const unsubscribe = springValue.on('change', (latest) => {
setDisplayValue(latest.toFixed(decimals));
});
return unsubscribe;
}, [springValue, decimals]);
return (
<span ref={ref} className="gradient-text-vibrant text-3xl lg:text-4xl font-bold">
{prefix}
{displayValue}
{suffix}
</span>
);
};
export const StatsCounter: React.FC<StatsCounterProps> = ({ stats }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
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.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.3 }}
className="text-center"
>
<div className="mb-2">
<AnimatedNumber
value={stat.value}
prefix={stat.prefix}
suffix={stat.suffix}
decimals={stat.decimals}
/>
</div>
<p className="text-xs font-medium text-gray-600">{stat.label}</p>
</motion.div>
))}
</motion.div>
);
};
/**
* Default stats - can be overridden with real data
* For now using generic/aspirational numbers
*/
export const defaultStats: Stat[] = [
{
label: 'QR Codes Created',
value: 850,
suffix: 'K+',
},
{
label: 'Total Scans',
value: 12.5,
suffix: 'M+',
decimals: 1,
},
{
label: 'Active Users',
value: 25,
suffix: 'K+',
},
{
label: 'Uptime',
value: 99.9,
suffix: '%',
decimals: 1,
},
];

249
src/lib/animations.ts Normal file
View File

@ -0,0 +1,249 @@
/**
* Centralized Animation Variants for Framer Motion
* Premium animation library for QR Master landing page
*/
import type { Variants } from 'framer-motion';
// Custom easing curves
export const easings = {
smooth: [0.25, 0.1, 0.25, 1],
snappy: [0.34, 1.56, 0.64, 1], // Bounce effect
elegant: [0.43, 0.13, 0.23, 0.96],
} as const;
// Fade & Scale animations
export const fadeIn: Variants = {
initial: { opacity: 0 },
animate: {
opacity: 1,
transition: { duration: 0.5, ease: easings.smooth }
},
};
export const scaleIn: Variants = {
initial: { opacity: 0, scale: 0.95 },
animate: {
opacity: 1,
scale: 1,
transition: { duration: 0.3, ease: easings.smooth }
},
};
export const scaleInBounce: Variants = {
initial: { opacity: 0, scale: 0.8 },
animate: {
opacity: 1,
scale: 1,
transition: { duration: 0.5, ease: easings.snappy }
},
};
// Slide animations
export const slideUp: Variants = {
initial: { opacity: 0, y: 30 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: easings.smooth }
},
};
export const slideUpBounce: Variants = {
initial: { opacity: 0, y: 30 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: easings.snappy }
},
};
export const slideDown: Variants = {
initial: { opacity: 0, y: -30 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: easings.smooth }
},
};
export const slideLeft: Variants = {
initial: { opacity: 0, x: 30 },
animate: {
opacity: 1,
x: 0,
transition: { duration: 0.5, ease: easings.smooth }
},
};
export const slideRight: Variants = {
initial: { opacity: 0, x: -30 },
animate: {
opacity: 1,
x: 0,
transition: { duration: 0.5, ease: easings.smooth }
},
};
// Container animations with stagger
export const staggerContainer: Variants = {
initial: {},
animate: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
}
},
};
export const staggerContainerFast: Variants = {
initial: {},
animate: {
transition: {
staggerChildren: 0.05,
delayChildren: 0,
}
},
};
export const staggerItem: Variants = {
initial: { opacity: 0, y: 20 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: easings.smooth }
},
};
// Morph animations (for QR code transformations)
export const colorMorph: Variants = {
initial: { scale: 1 },
animate: {
scale: [1, 1.02, 1],
transition: { duration: 0.4, ease: easings.smooth }
},
};
// Hover states
export const hoverScale = {
scale: 1.05,
transition: { duration: 0.2, ease: easings.smooth }
};
export const hoverScaleSmall = {
scale: 1.02,
transition: { duration: 0.2, ease: easings.smooth }
};
export const hoverLift = {
y: -4,
transition: { duration: 0.2, ease: easings.smooth }
};
export const hoverGlow = {
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.25)',
transition: { duration: 0.3, ease: easings.smooth }
};
// Rotate animations
export const rotate360: Variants = {
initial: { rotate: 0 },
animate: {
rotate: 360,
transition: { duration: 0.6, ease: easings.smooth }
},
};
export const pulseGlow: Variants = {
initial: { opacity: 0.7 },
animate: {
opacity: [0.7, 1, 0.7],
transition: {
duration: 2,
repeat: Infinity,
ease: 'easeInOut'
}
},
};
// Advanced entrance animations
export const revealFromBottom: Variants = {
initial: {
opacity: 0,
y: 50,
clipPath: 'inset(100% 0 0 0)'
},
animate: {
opacity: 1,
y: 0,
clipPath: 'inset(0% 0 0 0)',
transition: {
duration: 0.7,
ease: easings.elegant
}
},
};
// Scroll-triggered animation preset
export const scrollReveal = {
initial: "initial",
whileInView: "animate",
viewport: { once: true, margin: "-100px" }
} as const;
// Interactive button animations
export const buttonTap = {
scale: 0.95,
transition: { duration: 0.1 }
};
export const buttonHover = {
scale: 1.02,
boxShadow: '0 12px 32px rgba(99, 102, 241, 0.3)',
transition: { duration: 0.2 }
};
// Float animation (for decorative elements)
export const float: Variants = {
initial: { y: 0 },
animate: {
y: [-10, 10, -10],
transition: {
duration: 4,
repeat: Infinity,
ease: 'easeInOut'
}
}
};
// Shimmer effect
export const shimmer: Variants = {
initial: { backgroundPosition: '-200% 0' },
animate: {
backgroundPosition: '200% 0',
transition: {
duration: 2,
repeat: Infinity,
ease: 'linear'
}
}
};
// Drawing animation (for borders/lines)
export const drawLine: Variants = {
initial: { pathLength: 0, opacity: 0 },
animate: {
pathLength: 1,
opacity: 1,
transition: { duration: 1.5, ease: easings.smooth }
}
};
// Count-up animation helper
export const createCountUpVariant = (from: number, to: number, duration = 2): Variants => ({
initial: { value: from },
animate: {
value: to,
transition: { duration, ease: easings.smooth }
}
});

View File

@ -4,22 +4,21 @@
@layer utilities {
/* Floating blob animation for hero background */
@keyframes blob {
/* =======================
ANIMATIONS
======================= */
0%,
100% {
/* Floating blob animation for hero background (DEPRECATED - use grid instead) */
@keyframes blob {
0%, 100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(20px, -30px) scale(1.1);
}
50% {
transform: translate(-20px, 20px) scale(0.9);
}
75% {
transform: translate(30px, 10px) scale(1.05);
}
@ -40,6 +39,147 @@
.animation-delay-6000 {
animation-delay: 6s;
}
/* Grid dash animation for modern background */
@keyframes dashMove {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 24;
}
}
.animate-dash {
animation: dashMove 20s linear infinite;
}
/* Shimmer effect for loading/highlights */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.animate-shimmer {
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
/* Pulse glow for interactive elements */
@keyframes pulseGlow {
0%, 100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
}
.animate-pulse-glow {
animation: pulseGlow 2s ease-in-out infinite;
}
/* Float animation for decorative elements */
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
/* =======================
GLASSMORPHISM
======================= */
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
}
.glass-strong {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
.glass-dark {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* =======================
ADVANCED SHADOWS
======================= */
.shadow-primary-sm {
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1);
}
.shadow-primary-md {
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.15);
}
.shadow-primary-lg {
box-shadow: 0 20px 40px rgba(99, 102, 241, 0.2);
}
.shadow-success {
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.2);
}
.shadow-warning {
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2);
}
.shadow-elevation-1 {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.shadow-elevation-2 {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.shadow-elevation-3 {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.shadow-elevation-4 {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
/* Colored inner glow */
.glow-primary {
box-shadow: inset 0 0 20px rgba(99, 102, 241, 0.3);
}
.glow-success {
box-shadow: inset 0 0 20px rgba(16, 185, 129, 0.3);
}
}
:root {
@ -155,13 +295,82 @@ a {
@apply animate-pulse bg-gray-200 rounded;
}
/* Gradient backgrounds */
/* =======================
PREMIUM GRADIENTS
======================= */
/* Primary gradients */
.gradient-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-primary-vibrant {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%);
}
.gradient-primary-subtle {
background: linear-gradient(135deg, #eef2ff 0%, #f5f3ff 100%);
}
/* Success gradients */
.gradient-success {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.gradient-success-vibrant {
background: linear-gradient(135deg, #34d399 0%, #10b981 50%, #059669 100%);
}
/* Accent gradients */
.gradient-accent {
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
}
.gradient-warm {
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
}
.gradient-cool {
background: linear-gradient(135deg, #06b6d4 0%, #8b5cf6 100%);
}
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-text-vibrant {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Gradient borders */
.gradient-border {
border: 2px solid transparent;
background-clip: padding-box, border-box;
background-origin: padding-box, border-box;
background-image: linear-gradient(white, white), linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Animated gradient background */
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
.gradient-animated {
background: linear-gradient(270deg, #667eea, #764ba2, #8b5cf6);
background-size: 200% 200%;
animation: gradientShift 8s ease infinite;
}
/* Chart container */