Compare commits
2 Commits
master
...
feature/mo
| 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 React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
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 {
|
interface FeaturesProps {
|
||||||
t: any; // i18n translation function
|
t: any; // i18n translation function
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
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 (
|
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">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||||
|
{/* Section Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5 }}
|
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}
|
{t.features.title}
|
||||||
</h2>
|
</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>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
{/* Bento Grid */}
|
||||||
{features.map((feature, index) => (
|
<BentoGrid>
|
||||||
<motion.div
|
{/* Analytics - Large */}
|
||||||
key={feature.key}
|
<BentoItem size="large">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<FeatureAnalyticsDemo />
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
</BentoItem>
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
{/* Customization - Medium */}
|
||||||
>
|
<BentoItem size="medium">
|
||||||
<Card hover className="h-full border-gray-100 hover:border-primary-100 hover:shadow-lg transition-all">
|
<FeatureCustomizationDemo />
|
||||||
<CardHeader>
|
</BentoItem>
|
||||||
<div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}>
|
|
||||||
{feature.icon}
|
{/* Bulk Creation - Medium */}
|
||||||
</div>
|
<BentoItem size="medium">
|
||||||
<CardTitle>{t.features[feature.key].title}</CardTitle>
|
<FeatureBulkDemo />
|
||||||
</CardHeader>
|
</BentoItem>
|
||||||
<CardContent>
|
|
||||||
<p className="text-gray-600">
|
{/* Unlimited - Small */}
|
||||||
{t.features[feature.key].description}
|
<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>
|
</p>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,152 +4,156 @@ import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Card } from '@/components/ui/Card';
|
|
||||||
import { motion } from 'framer-motion';
|
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 {
|
interface HeroProps {
|
||||||
t: any; // i18n translation function
|
t: any; // i18n translation function
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Hero: React.FC<HeroProps> = ({ t }) => {
|
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 (
|
return (
|
||||||
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
|
<section className="relative overflow-hidden pt-20 pb-32 min-h-[90vh] flex items-center">
|
||||||
{/* Animated Background Orbs */}
|
{/* Modern Animated Background */}
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
<AnimatedBackground />
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
|
<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">
|
{/* Main Content Grid */}
|
||||||
{/* Left Content */}
|
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||||
<div className="space-y-8">
|
{/* Left: Content */}
|
||||||
<Badge variant="info" className="inline-flex items-center space-x-2">
|
|
||||||
<span>{t.hero.badge}</span>
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
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"
|
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}
|
{t.hero.title}
|
||||||
</h2>
|
</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}
|
{t.hero.subtitle}
|
||||||
</p>
|
</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) => (
|
{t.hero.features.map((feature: string, index: number) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.2 + (index * 0.1) }}
|
transition={{ delay: 0.4 + index * 0.08, duration: 0.3 }}
|
||||||
className="flex items-center space-x-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">
|
<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-emerald-600" />
|
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-700 font-medium">{feature}</span>
|
<span className="text-gray-700 font-medium text-lg">{feature}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.6, duration: 0.3 }}
|
||||||
className="flex flex-col sm:flex-row gap-4 pt-4"
|
className="flex flex-col sm:flex-row gap-4 pt-6 justify-center lg:justify-start"
|
||||||
>
|
>
|
||||||
<Link href="/signup">
|
<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">
|
<Button
|
||||||
{t.hero.cta_primary}
|
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>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/#pricing">
|
<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}
|
{t.hero.cta_secondary}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Preview Widget */}
|
{/* Trust Indicator */}
|
||||||
<div className="relative">
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerjs}
|
initial={{ opacity: 0 }}
|
||||||
initial="hidden"
|
animate={{ opacity: 1 }}
|
||||||
animate="show"
|
transition={{ delay: 0.8, duration: 0.3 }}
|
||||||
className="grid grid-cols-2 gap-4"
|
className="flex items-center gap-2 text-sm text-gray-500 justify-center lg:justify-start"
|
||||||
>
|
>
|
||||||
{templateCards.map((card, index) => (
|
<div className="flex -space-x-2">
|
||||||
<motion.div key={index} variants={itemjs}>
|
{[1, 2, 3].map((i) => (
|
||||||
<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
|
||||||
<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`}>
|
key={i}
|
||||||
<card.icon className="w-6 h-6" />
|
className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-400 border-2 border-white"
|
||||||
</div>
|
/>
|
||||||
<p className="font-semibold text-gray-800 group-hover:text-gray-900">{card.title}</p>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
<span>Trusted by 25,000+ professionals worldwide</span>
|
||||||
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Floating Badge */}
|
{/* Right: QR Types Showcase + Phone */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, x: 50 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.8 }}
|
transition={{ delay: 0.3, duration: 0.5 }}
|
||||||
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"
|
className="relative space-y-8"
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2">
|
{/* QR Types Grid */}
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
|
<QRTypesShowcase />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
|
|
||||||
</span>
|
{/* Phone Mockup with Auto-Rotation */}
|
||||||
{t.hero.engagement_badge}
|
<div className="flex justify-center">
|
||||||
|
<PhoneMockup autoRotate={true} />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Counter - Subtiler */}
|
||||||
|
<div className="mt-20">
|
||||||
|
<StatsCounter stats={defaultStats} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Smooth Gradient Fade Transition */}
|
{/* 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-gray-50 pointer-events-none" />
|
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-white pointer-events-none" />
|
||||||
</section >
|
</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 React, { useState } from 'react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { calculateContrast } from '@/lib/utils';
|
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 {
|
interface InstantGeneratorProps {
|
||||||
t: any; // i18n translation function
|
t: any; // i18n translation function
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TabType = 'basic' | 'presets' | 'advanced';
|
||||||
|
|
||||||
export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
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 [foregroundColor, setForegroundColor] = useState('#000000');
|
||||||
const [backgroundColor, setBackgroundColor] = useState('#FFFFFF');
|
const [backgroundColor, setBackgroundColor] = useState('#FFFFFF');
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState('bold-1');
|
||||||
const [cornerStyle, setCornerStyle] = useState('square');
|
const [cornerStyle, setCornerStyle] = useState('square');
|
||||||
const [size, setSize] = useState(200);
|
const [size, setSize] = useState(256);
|
||||||
|
|
||||||
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
const contrast = calculateContrast(foregroundColor, backgroundColor);
|
||||||
const hasGoodContrast = contrast >= 4.5;
|
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 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 (!svg || !url) return;
|
||||||
|
|
||||||
if (format === 'svg') {
|
if (format === 'svg') {
|
||||||
|
|
@ -40,13 +58,12 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(downloadUrl);
|
URL.revokeObjectURL(downloadUrl);
|
||||||
} else {
|
} else {
|
||||||
// Convert SVG to PNG using Canvas
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const svgData = new XMLSerializer().serializeToString(svg);
|
const svgData = new XMLSerializer().serializeToString(svg);
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||||
const url = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
|
|
@ -56,9 +73,9 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(0, 0, size, size);
|
||||||
ctx.drawImage(img, 0, 0, size, size);
|
ctx.drawImage(img, 0, 0, size, size);
|
||||||
}
|
}
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((canvasBlob) => {
|
||||||
if (blob) {
|
if (canvasBlob) {
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
const downloadUrl = URL.createObjectURL(canvasBlob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = downloadUrl;
|
a.href = downloadUrl;
|
||||||
a.download = 'qrcode.png';
|
a.download = 'qrcode.png';
|
||||||
|
|
@ -68,19 +85,16 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
URL.revokeObjectURL(downloadUrl);
|
URL.revokeObjectURL(downloadUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(blobUrl);
|
||||||
};
|
};
|
||||||
img.src = url;
|
img.src = blobUrl;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="pt-16 pb-32 bg-gray-50 border-t border-gray-100 relative">
|
<section className="py-20 bg-gradient-to-b from-white to-gray-50">
|
||||||
<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)' }}
|
|
||||||
/>
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||||
|
{/* Section Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
|
@ -88,83 +102,141 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="text-center mb-12"
|
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}
|
{t.generator.title}
|
||||||
</h2>
|
</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>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
|
<div className="grid lg:grid-cols-2 gap-8 lg:gap-12 max-w-6xl mx-auto">
|
||||||
{/* Left Form */}
|
{/* Left: Controls */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
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
|
<Input
|
||||||
label="URL"
|
label="Your URL"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
placeholder={t.generator.url_placeholder}
|
placeholder="https://your-website.com"
|
||||||
className="transition-all focus:ring-2 focus:ring-primary-500/20"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="foreground-color" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t.generator.foreground}
|
Foreground
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
id="foreground-color"
|
|
||||||
type="color"
|
type="color"
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="w-14 h-12 rounded border border-gray-300 cursor-pointer"
|
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
|
||||||
aria-label="Foreground color picker"
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
id="foreground-color-text"
|
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1 font-mono text-sm"
|
||||||
aria-label="Foreground color hex value"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t.generator.background}
|
Background
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
id="background-color"
|
|
||||||
type="color"
|
type="color"
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
className="w-14 h-12 rounded border border-gray-300 cursor-pointer"
|
className="w-14 h-12 rounded-lg border border-gray-300 cursor-pointer"
|
||||||
aria-label="Background color picker"
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
id="background-color-text"
|
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1 font-mono text-sm"
|
||||||
aria-label="Background color hex value"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t.generator.corners}
|
Corner Style
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="corner-style"
|
|
||||||
value={cornerStyle}
|
value={cornerStyle}
|
||||||
onChange={(e) => setCornerStyle(e.target.value)}
|
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"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t.generator.size}
|
Size: {size}px
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="qr-size"
|
|
||||||
type="range"
|
type="range"
|
||||||
min="100"
|
min="128"
|
||||||
max="400"
|
max="512"
|
||||||
|
step="32"
|
||||||
value={size}
|
value={size}
|
||||||
onChange={(e) => setSize(Number(e.target.value))}
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
className="w-full accent-primary-600"
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||||
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
|
<p className="text-sm text-blue-900">
|
||||||
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
|
<strong>Pro Tip:</strong> Higher resolution is better for print materials. For digital use, 256px is usually sufficient.
|
||||||
</Badge>
|
</p>
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Contrast: {contrast.toFixed(1)}:1
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
{/* Download Actions */}
|
||||||
<Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('svg')}>
|
<div className="mt-8 pt-6 border-t border-gray-100 space-y-3">
|
||||||
{t.generator.download_svg}
|
<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>
|
||||||
<Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('png')}>
|
<Button
|
||||||
{t.generator.download_png}
|
variant="outline"
|
||||||
|
onClick={() => downloadQR('png')}
|
||||||
|
className="hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
PNG
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
<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'}>
|
className="w-full shadow-primary-md hover:shadow-primary-lg transition-all"
|
||||||
{t.generator.save_track}
|
onClick={() => window.location.href = '/signup'}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Save & Track Analytics
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Right Preview */}
|
{/* Right: Preview */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: 0.4 }}
|
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
|
<QRCodeSVG
|
||||||
value={url}
|
value={url}
|
||||||
size={size}
|
size={size}
|
||||||
fgColor={foregroundColor}
|
fgColor={foregroundColor}
|
||||||
bgColor={backgroundColor}
|
bgColor={backgroundColor}
|
||||||
level="M"
|
level="H"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
|
{/* Phone Mockup Preview */}
|
||||||
|
<PhoneMockup
|
||||||
|
url={url}
|
||||||
|
foreground={foregroundColor}
|
||||||
|
background={backgroundColor}
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</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 (
|
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">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5 }}
|
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}
|
{t.pricing.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-gray-600">
|
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||||
{t.pricing.subtitle}
|
{t.pricing.subtitle}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -63,11 +63,15 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
||||||
className="h-full"
|
className="h-full"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={`h-full flex flex-col ${plan.popular
|
className={`h-full flex flex-col relative overflow-hidden ${plan.popular
|
||||||
? 'border-primary-500 shadow-xl relative scale-105 z-10'
|
? 'border-primary-500 shadow-primary-lg scale-105 z-10 bg-gradient-to-br from-white to-blue-50'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
|
: '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 && (
|
{plan.popular && (
|
||||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
|
<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">
|
<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 {
|
@layer utilities {
|
||||||
|
|
||||||
/* Floating blob animation for hero background */
|
/* =======================
|
||||||
@keyframes blob {
|
ANIMATIONS
|
||||||
|
======================= */
|
||||||
|
|
||||||
0%,
|
/* Floating blob animation for hero background (DEPRECATED - use grid instead) */
|
||||||
100% {
|
@keyframes blob {
|
||||||
|
0%, 100% {
|
||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
transform: translate(20px, -30px) scale(1.1);
|
transform: translate(20px, -30px) scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: translate(-20px, 20px) scale(0.9);
|
transform: translate(-20px, 20px) scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
transform: translate(30px, 10px) scale(1.05);
|
transform: translate(30px, 10px) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +39,147 @@
|
||||||
.animation-delay-6000 {
|
.animation-delay-6000 {
|
||||||
animation-delay: 6s;
|
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 {
|
:root {
|
||||||
|
|
@ -155,13 +295,82 @@ a {
|
||||||
@apply animate-pulse bg-gray-200 rounded;
|
@apply animate-pulse bg-gray-200 rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient backgrounds */
|
/* =======================
|
||||||
|
PREMIUM GRADIENTS
|
||||||
|
======================= */
|
||||||
|
|
||||||
|
/* Primary gradients */
|
||||||
.gradient-primary {
|
.gradient-primary {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
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 {
|
.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 */
|
/* Chart container */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue