Compare commits
2 Commits
7328b3240d
...
3c6d75b6bb
| Author | SHA1 | Date |
|---|---|---|
|
|
3c6d75b6bb | |
|
|
a7180e3b9b |
Binary file not shown.
|
After Width: | Height: | Size: 545 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
{/* 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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={{ 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>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{/* Feature List */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.4 }}
|
||||
className="space-y-4 pt-4"
|
||||
>
|
||||
{t.hero.features.map((feature: string, index: number) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 + (index * 0.1) }}
|
||||
className="flex items-center space-x-3"
|
||||
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-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||
<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">{feature}</span>
|
||||
<span className="text-gray-700 font-medium text-lg">{feature}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* CTAs */}
|
||||
<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"
|
||||
transition={{ delay: 0.6, duration: 0.3 }}
|
||||
className="flex flex-col sm:flex-row gap-4 pt-6 justify-center lg:justify-start"
|
||||
>
|
||||
<Link href="/signup">
|
||||
<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
|
||||
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-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">
|
||||
<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>
|
||||
|
||||
{/* Right Preview Widget */}
|
||||
<div className="relative">
|
||||
{/* Trust Indicator */}
|
||||
<motion.div
|
||||
variants={containerjs}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-2 gap-4"
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
</motion.div>
|
||||
<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>
|
||||
|
||||
{/* Floating Badge */}
|
||||
{/* Right: QR Types Showcase + Phone */}
|
||||
<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, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
className="relative space-y-8"
|
||||
>
|
||||
<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}
|
||||
{/* 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" />
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,83 +102,141 @@ 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">
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</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="URL"
|
||||
label="Your 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"
|
||||
placeholder="https://your-website.com"
|
||||
/>
|
||||
|
||||
<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 className="block text-sm font-medium text-gray-700 mb-2">
|
||||
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"
|
||||
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
id="foreground-color-text"
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
aria-label="Foreground color hex value"
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.background}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
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"
|
||||
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
id="background-color-text"
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
aria-label="Background color hex value"
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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 htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.corners}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Corner Style
|
||||
</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"
|
||||
|
|
@ -175,106 +247,98 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.size}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Size: {size}px
|
||||
</label>
|
||||
<input
|
||||
id="qr-size"
|
||||
type="range"
|
||||
min="100"
|
||||
max="400"
|
||||
min="128"
|
||||
max="512"
|
||||
step="32"
|
||||
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 className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>128px</span>
|
||||
<span>512px</span>
|
||||
</div>
|
||||
</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 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>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('svg')}>
|
||||
{t.generator.download_svg}
|
||||
{/* 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" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('png')}>
|
||||
{t.generator.download_png}
|
||||
<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 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
|
||||
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>
|
||||
</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"
|
||||
>
|
||||
{/* 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' : ''
|
||||
}`}
|
||||
>
|
||||
{/* 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>
|
||||
</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"
|
||||
level="H"
|
||||
/>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
@ -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 }
|
||||
}
|
||||
});
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue